diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index ab063e29e..4b15e6de8 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -37,7 +37,7 @@ jobs: python-version: "3.12" - name: Install dependencies - run: pip install -e "python/[dev,serve]" + run: pip install -e "python/[dev,convert]" - name: Run tests run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6766a43a..c79543413 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: version=$(python3 -c "raw='${raw}'; print(raw + '.0' if raw.count('.') == 1 else raw)") echo "version=$version" >> "$GITHUB_OUTPUT" - - name: Resolve platform wheel URLs and SHA256 from PyPI + - name: Resolve sdist URL and SHA256 from PyPI id: pypi env: VERSION: ${{ steps.ver.outputs.version }} @@ -47,22 +47,18 @@ jobs: sleep 30 done [ -n "$json" ] || { echo "PyPI metadata not available for ${PACKAGE} ${VERSION}"; exit 1; } - pick() { echo "$json" | jq -er --arg re "$1" '.urls[] | select(.packagetype=="bdist_wheel" and (.filename|test($re))) | .'"$2"; } + pick() { echo "$json" | jq -er '.urls[] | select(.packagetype=="sdist") | .'"$1"; } { - echo "macos_url=$(pick macosx url)" - echo "macos_sha=$(pick macosx digests.sha256)" - echo "linux_url=$(pick manylinux url)" - echo "linux_sha=$(pick manylinux digests.sha256)" + echo "sdist_url=$(pick url)" + echo "sdist_sha=$(pick digests.sha256)" } >> "$GITHUB_OUTPUT" - name: Update tap formula env: HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} VERSION: ${{ steps.ver.outputs.version }} - MACOS_URL: ${{ steps.pypi.outputs.macos_url }} - MACOS_SHA: ${{ steps.pypi.outputs.macos_sha }} - LINUX_URL: ${{ steps.pypi.outputs.linux_url }} - LINUX_SHA: ${{ steps.pypi.outputs.linux_sha }} + SDIST_URL: ${{ steps.pypi.outputs.sdist_url }} + SDIST_SHA: ${{ steps.pypi.outputs.sdist_sha }} run: | git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/${TAP}.git" tap mkdir -p tap/Formula @@ -70,22 +66,15 @@ jobs: class Cactus < Formula desc "On-device AI inference" homepage "https://cactuscompute.com" + url "${SDIST_URL}" + sha256 "${SDIST_SHA}" version "${VERSION}" license "Cactus Compute License" include Language::Python::Virtualenv depends_on "python@3.12" + depends_on macos: :sonoma depends_on arch: :arm64 - on_macos do - url "${MACOS_URL}" - sha256 "${MACOS_SHA}" - end - - on_linux do - url "${LINUX_URL}" - sha256 "${LINUX_SHA}" - end - livecheck do url "https://pypi.org/pypi/${PACKAGE}/json" regex(/"version":\s*"(\d+(?:\.\d+)+)"/i) @@ -93,13 +82,19 @@ jobs: def install virtualenv_create(libexec, "python3.12") - wheel = Dir[buildpath/"*.whl"].first - odie "no wheel found in build directory" if wheel.nil? system Formula["python@3.12"].opt_bin/"python3.12", "-m", "pip", - "--python=#{libexec}/bin/python", "install", "--no-deps", wheel + "--python=#{libexec}/bin/python", "install", "${PACKAGE}==#{version}" bin.install_symlink libexec/"bin/cactus" end + def caveats + <<~EOS + cactus code needs Node.js >= 22 on PATH. + cactus convert (torch) is opt-in, same as pip's [convert] extra: + #{Formula["python@3.12"].opt_bin}/python3.12 -m pip --python=#{libexec}/bin/python install "${PACKAGE}[convert]==#{version}" + EOS + end + test do assert_equal "cactus #{version}\n", shell_output("#{bin}/cactus --version") assert_match "libcactus_engine", diff --git a/python/cactus/cli/code.py b/python/cactus/cli/code.py index 090528544..8c35449c5 100644 --- a/python/cactus/cli/code.py +++ b/python/cactus/cli/code.py @@ -14,12 +14,14 @@ def _find_agent_cli() -> Path | None: - bundled = _PACKAGE_DIR / "code" / "packages" / "coding-agent" / "dist" / "cli.js" - if bundled.exists(): - return bundled - dev = _PROJECT_ROOT / "cactus-code" / "packages" / "coding-agent" / "dist" / "cli.js" - if dev.exists(): - return dev + candidates = ( + _PACKAGE_DIR / "code" / "node_modules" / "@earendil-works" / "pi-coding-agent" / "dist" / "cli.js", + _PACKAGE_DIR / "code" / "packages" / "coding-agent" / "dist" / "cli.js", + _PROJECT_ROOT / "cactus-code" / "packages" / "coding-agent" / "dist" / "cli.js", + ) + for cli in candidates: + if cli.exists(): + return cli return None @@ -170,6 +172,7 @@ def cmd_code(args) -> int: env = dict(os.environ) env["CACTUS_BASE_URL"] = base_url + env["PI_PACKAGE_DIR"] = str(agent_cli.parent.parent) agent_args = [a for a in (args.agent_args or []) if a != "--"] interactive = not any(a in ("-p", "--print") for a in agent_args) if interactive and (started_server is not None or agent_was_built): diff --git a/python/pyproject.toml b/python/pyproject.toml index 692623d2d..0198bb301 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -30,18 +30,20 @@ classifiers = [ dependencies = [ "numpy>=1.26.0,<3", "huggingface-hub>=1.5.0,<2.0", + "fastapi>=0.100.0", + "uvicorn>=0.20.0", + "python-multipart>=0.0.5", + "Pillow>=11.0.0,<12", ] [project.optional-dependencies] dev = ["pytest>=8.0", "httpx>=0.25.0", "einops>=0.7"] lora = ["peft>=0.15"] -serve = ["fastapi>=0.100.0", "uvicorn>=0.20.0", "python-multipart>=0.0.5"] convert = [ "torch>=2.8.0,<3", "torchvision>=0.23.0,<1", "transformers==5.5.4", "scipy>=1.13", - "Pillow>=11.0.0,<12", "sentencepiece>=0.2.0", ] diff --git a/python/tests/test_bindings.py b/python/tests/test_bindings.py index a84f8d725..60f832fd5 100644 --- a/python/tests/test_bindings.py +++ b/python/tests/test_bindings.py @@ -22,7 +22,7 @@ class TestPackageStructure: def test_version_exists(self): from cactus import __version__ - assert __version__ == "2.0.0" + assert __version__ and __version__[0].isdigit() def test_cli_module_importable(self): from cactus.cli import main, create_parser