diff --git a/chipfoundry_cli/__init__.py b/chipfoundry_cli/__init__.py index defdffe..a11258f 100644 --- a/chipfoundry_cli/__init__.py +++ b/chipfoundry_cli/__init__.py @@ -1,2 +1,2 @@ """ChipFoundry CLI package: Automate project submission to SFTP.""" -__version__ = "2.7.0" \ No newline at end of file +__version__ = "2.7.1" \ No newline at end of file diff --git a/chipfoundry_cli/main.py b/chipfoundry_cli/main.py index 04cffbc..f23acc6 100644 --- a/chipfoundry_cli/main.py +++ b/chipfoundry_cli/main.py @@ -3661,8 +3661,13 @@ def maybe_abort_no_space(err, step_label): ) # Install from GitHub source (workaround: PyPI package is under efabless, we use chipfoundry repo) env = os.environ.copy() - if sys.platform == 'darwin': - env['LDSHARED'] = 'clang++ -dynamiclib -undefined dynamic_lookup' + # cocotb 1.9.2 rewrites -bundle to -dynamiclib on the stdlib distutils + # config, but an isolated pip build links with setuptools' vendored + # distutils and never sees that change. Python 3.12+ has no stdlib + # distutils, so this only applies to older interpreters. The venv is + # created with sys.executable, so sys.version_info matches it. + if sys.platform == 'darwin' and sys.version_info < (3, 12): + env['SETUPTOOLS_USE_DISTUTILS'] = 'stdlib' subprocess.run( [ venv_python, '-m', 'pip', 'install', '--upgrade', '--no-cache-dir', @@ -3710,9 +3715,22 @@ def maybe_abort_no_space(err, step_label): except subprocess.CalledProcessError as e: maybe_abort_no_space(e, "Cocotb setup") had_errors = True - console.print(f"[red]✗[/red] Failed to set up Cocotb: {e}") - if e.stderr: - console.print(f"[dim]{e.stderr}[/dim]") + cmd_parts = e.cmd if isinstance(e.cmd, (list, tuple)) else [str(e.cmd)] + is_cocotb_install = any( + "caravel-sim-infrastructure" in str(part) for part in cmd_parts + ) + if is_cocotb_install: + console.print("[red]✗[/red] Failed to set up Cocotb") + stderr = e.stderr or b"" + if isinstance(stderr, bytes): + stderr = stderr.decode(errors="replace") + tail = "\n".join(stderr.splitlines()[-25:]) + if tail: + console.print(f"[dim]{tail}[/dim]") + else: + console.print(f"[red]✗[/red] Failed to set up Cocotb: {e}") + if e.stderr: + console.print(f"[dim]{e.stderr}[/dim]") except Exception as e: maybe_abort_no_space(e, "Cocotb setup") had_errors = True diff --git a/pyproject.toml b/pyproject.toml index 2e4945b..12ba02f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chipfoundry-cli" -version = "2.7.0" +version = "2.7.1" description = "CLI tool to automate ChipFoundry project submission to SFTP server" authors = ["ChipFoundry "] readme = "README.md" diff --git a/tests/test_setup.py b/tests/test_setup.py index d05c7ec..05483f2 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -7,6 +7,7 @@ from pathlib import Path import json import shutil +import subprocess import tempfile import os @@ -303,6 +304,77 @@ def test_setup_only_flags_with_overwrite(self, temp_project_dir): assert result.exit_code in [0, 1] assert 'caravel' in result.output.lower() or 'dry' in result.output.lower() or 'version' in result.output.lower() + def _init_project(self, project_dir): + cf_dir = Path(project_dir) / '.cf' + cf_dir.mkdir(parents=True, exist_ok=True) + (cf_dir / 'project.json').write_text(json.dumps({ + "project": {"name": "cocotb-test", "type": "digital"} + })) + + def _cocotb_install_env(self, temp_project_dir, monkeypatch, platform, version_info): + import chipfoundry_cli.main as cf_main + + self._init_project(temp_project_dir) + monkeypatch.delenv("LDSHARED", raising=False) + monkeypatch.delenv("SETUPTOOLS_USE_DISTUTILS", raising=False) + monkeypatch.setattr(cf_main.sys, "platform", platform) + monkeypatch.setattr(cf_main.sys, "version_info", version_info) + calls = [] + + def fake_run(cmd, *args, **kwargs): + calls.append((list(cmd), kwargs.get("env"))) + return subprocess.CompletedProcess(cmd, 0) + + monkeypatch.setattr(cf_main.subprocess, "run", fake_run) + result = CliRunner().invoke(main, [ + 'setup', '--project-root', temp_project_dir, '--only-cocotb', '--overwrite', + ]) + assert result.exit_code == 0, result.output + installs = [ + env for cmd, env in calls + if any('caravel-sim-infrastructure' in str(part) for part in cmd) + ] + assert len(installs) == 1 + return installs[0] + + def test_cocotb_install_uses_stdlib_distutils_on_macos_before_312(self, temp_project_dir, monkeypatch): + env = self._cocotb_install_env(temp_project_dir, monkeypatch, 'darwin', (3, 11, 8)) + assert env['SETUPTOOLS_USE_DISTUTILS'] == 'stdlib' + assert 'LDSHARED' not in env + + def test_cocotb_install_skips_stdlib_distutils_on_python_312(self, temp_project_dir, monkeypatch): + env = self._cocotb_install_env(temp_project_dir, monkeypatch, 'darwin', (3, 12, 0)) + assert 'SETUPTOOLS_USE_DISTUTILS' not in env + assert 'LDSHARED' not in env + + def test_cocotb_install_skips_stdlib_distutils_off_macos(self, temp_project_dir, monkeypatch): + env = self._cocotb_install_env(temp_project_dir, monkeypatch, 'linux', (3, 11, 8)) + assert 'SETUPTOOLS_USE_DISTUTILS' not in env + assert 'LDSHARED' not in env + + def test_cocotb_install_failure_prints_stderr_tail(self, temp_project_dir, monkeypatch): + import chipfoundry_cli.main as cf_main + + self._init_project(temp_project_dir) + log = "\n".join(f"copying file {i}" for i in range(40)).encode() + + def fake_run(cmd, *args, **kwargs): + if any('caravel-sim-infrastructure' in str(part) for part in cmd): + raise subprocess.CalledProcessError(1, cmd, stderr=log) + return subprocess.CompletedProcess(cmd, 0) + + monkeypatch.setattr(cf_main.subprocess, "run", fake_run) + result = CliRunner().invoke(main, [ + 'setup', '--project-root', temp_project_dir, '--only-cocotb', '--overwrite', + ]) + assert result.exit_code == 1 + assert "Failed to set up Cocotb" in result.output + assert "CalledProcessError" not in result.output + assert "copying file 15" in result.output + assert "copying file 39" in result.output + assert "copying file 14" not in result.output + assert "Setup completed with errors" in result.output + if __name__ == '__main__': pytest.main([__file__, '-v'])