Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ on:
- "v*.*.*"

permissions:
contents: write
contents: write
id-token: write # PyPI Trusted Publishing (OIDC) -- no API token needed

jobs:
release:
Expand Down Expand Up @@ -40,9 +41,18 @@ jobs:
git push --delete origin ${GITHUB_REF#refs/tags/}
exit 1 # Fail the workflow

- name: Resolve version
# The tag must match gradle.properties: it names the jar, the wheel, and the GitHub Release,
# and a mismatch would publish the wrong version to PyPI.
- name: Resolve and verify version
id: ver
run: echo "version=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
run: |
version="${GITHUB_REF#refs/tags/v}"
declared="$(sed -n 's/^version=//p' gradle.properties)"
if [[ "$version" != "$declared" ]]; then
echo "::error::Tag version ($version) != gradle.properties version ($declared). Bump gradle.properties or fix the tag."
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"

# Stage release assets: a stable-named codeanalyzer.jar (what the installer fetches), the
# Neo4j schema contract (platform-independent, version-locked to this build), and the
Expand All @@ -56,6 +66,27 @@ jobs:
cp packaging/install/codeanalyzer-installer.sh release-assets/codeanalyzer-installer.sh
ls -lh release-assets

# The PyPI package: the same jar plus a `canjv` launcher, on a jdk4py runtime (see
# packaging/python). One pure wheel; the JVM comes from jdk4py's own platform wheels.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Build the Python wheel
env:
PKG_VERSION: ${{ steps.ver.outputs.version }}
run: |
python -m pip install --upgrade build hatchling
./packaging/python/build_wheel.sh

- name: Publish wheel to PyPI (Trusted Publishing / OIDC)
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: packaging/python/dist
# Re-runs stay idempotent: a version already on PyPI is skipped, not a 400.
skip-existing: true

- name: Build Changelog
id: gen_changelog
uses: mikepenz/release-changelog-builder-action@v6
Expand All @@ -73,6 +104,12 @@ jobs:
echo "## Install"
echo
echo '```sh'
echo "pip install codeanalyzer-java==${{ steps.ver.outputs.version }} # bundles a JVM; installs the canjv launcher"
echo '```'
echo
echo "Or the jar with a \`codeanalyzer\` launcher (requires Java 11+):"
echo
echo '```sh'
echo "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/${GITHUB_REPOSITORY}/releases/download/v${{ steps.ver.outputs.version }}/codeanalyzer-installer.sh | sh"
echo '```'
echo
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ and emits them either as the canonical `analysis.json`, or as a **Neo4j property

## Quick install

Grab the latest release jar and a `codeanalyzer` launcher (requires a Java 11+ runtime):
From PyPI, with a bundled JVM (no system Java needed). Installs a `canjv` launcher:

```sh
pip install codeanalyzer-java
canjv -i /path/to/project -a 2 -o ./out
```

Or grab the latest release jar and a `codeanalyzer` launcher (requires a Java 11+ runtime):

```sh
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/codellm-devkit/codeanalyzer-java/releases/latest/download/codeanalyzer-installer.sh | sh
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=3.0.1
version=3.0.2
2 changes: 2 additions & 0 deletions packaging/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
README.md
dist/
57 changes: 57 additions & 0 deletions packaging/python/build_wheel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
#
# Build the codeanalyzer-java wheel: the fat JAR under _bin/ plus a `canjv` launcher that runs it
# on the jdk4py runtime. The JAR is platform-independent and the JVM comes from jdk4py's own
# platform wheels, so exactly one py3-none-any wheel is produced.
#
# Requirements on the build host:
# - the fat JAR already built: ./gradlew fatJar (build/libs/codeanalyzer-<version>.jar)
# - python -m pip install build hatchling
#
# Usage:
# PKG_VERSION=3.0.2 ./build_wheel.sh # writes ./dist/codeanalyzer_java-3.0.2-py3-none-any.whl
# twine upload dist/*.whl # publish
#
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$HERE/../.." && pwd)"
PKG_VERSION="${PKG_VERSION:-$(sed -n 's/^version=//p' "$REPO_ROOT/gradle.properties")}"
JAR="${JAR:-$REPO_ROOT/build/libs/codeanalyzer-${PKG_VERSION}.jar}"
BIN_DIR="$HERE/src/codeanalyzer_java/_bin"
INIT_PY="$HERE/src/codeanalyzer_java/__init__.py"

[[ -f "$JAR" ]] || { echo "error: $JAR not found; run ./gradlew fatJar first" >&2; exit 1; }

# Stamp $PKG_VERSION into __init__.py for the build and restore it on exit; likewise remove the
# copied JAR and README, so the working tree stays pristine.
ORIG_INIT="$(cat "$INIT_PY")"
cleanup() {
printf '%s\n' "$ORIG_INIT" > "$INIT_PY"
find "$BIN_DIR" -mindepth 1 ! -name '.gitignore' -delete
rm -f "$HERE/README.md"
}
trap cleanup EXIT
python - "$INIT_PY" "$PKG_VERSION" <<'PY'
import re, sys
path, version = sys.argv[1], sys.argv[2]
text = open(path).read()
new, n = re.subn(r'__version__ = "[^"]*"', f'__version__ = "{version}"', text)
if n != 1:
raise SystemExit(f"expected exactly one __version__ assignment in {path}, found {n}")
open(path, "w").write(new)
print(f">>> stamped __version__ = {version}")
PY

rm -rf "$HERE/dist"
mkdir -p "$HERE/dist" "$BIN_DIR"
cp "$JAR" "$BIN_DIR/codeanalyzer.jar"
cp "$REPO_ROOT/README.md" "$HERE/README.md"

python -m build --wheel --no-isolation -o "$HERE/dist" "$HERE"

echo
echo ">>> Built wheel:"
ls -lh "$HERE/dist"/*.whl
echo
echo "Publish with: twine upload $HERE/dist/*.whl"
57 changes: 57 additions & 0 deletions packaging/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "codeanalyzer-java"
dynamic = ["version"]
description = "codeanalyzer-java backend for CLDK (codellm-devkit): the analyzer JAR plus a bundled JVM."
# The PyPI long description is the repo root README, copied in by build_wheel.sh at build time
# (single source of truth; the copy is gitignored).
readme = "README.md"
requires-python = ">=3.8"
license = { text = "Apache-2.0" }
authors = [
{ name = "Rahul Krishna", email = "i.m.ralk@gmail.com" },
]
keywords = ["cldk", "codellm-devkit", "java", "code analysis", "wala"]
classifiers = [
"Development Status :: 4 - Beta",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Java",
]
# jdk4py ships a jlink'd Temurin runtime per platform, so this wheel is pure (py3-none-any) and
# pip picks the right JVM. Pinned below 22: WALA 1.6.x's Shrike reads class files up to version 67
# (Java 23), and jdk4py 25 ships version-69 class files that it rejects. Bump this with WALA.
dependencies = ["jdk4py>=21,<22"]

[project.urls]
Homepage = "https://codellm-devkit.info"
Repository = "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/codellm-devkit/codeanalyzer-java"
Issues = "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/codellm-devkit/codeanalyzer-java/issues"

# `canjv` follows the sibling analyzers (canpy, cants, caniac). There is deliberately no
# `codeanalyzer` script: codeanalyzer-python already installs one as a deprecated alias, and two
# packages claiming the same console script silently overwrite each other in one environment.
[project.scripts]
canjv = "codeanalyzer_java.__main__:main"

# Single source of truth for the package version: __init__.py's __version__.
# build_wheel.sh overwrites it from $PKG_VERSION (set from gradle.properties in CI), so a
# release of vX.Y.Z always produces a codeanalyzer-java==X.Y.Z wheel.
[tool.hatch.version]
path = "src/codeanalyzer_java/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["src/codeanalyzer_java"]
artifacts = ["src/codeanalyzer_java/_bin/*"]

[tool.hatch.build.targets.sdist]
# No meaningful sdist: the JAR needs Gradle + a JDK to build. Ship the wheel only.
include = [
"src/codeanalyzer_java/__init__.py",
"README.md",
"build_wheel.sh",
]
52 changes: 52 additions & 0 deletions packaging/python/src/codeanalyzer_java/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Prebuilt ``codeanalyzer-java`` backend for CLDK, with a bundled JVM.

This package carries the self-contained analyzer JAR and depends on ``jdk4py`` for a
Temurin runtime, so ``pip install codeanalyzer-java`` needs no system Java. CLDK's Python
SDK depends on this package and calls :func:`command` to run the analyzer, exactly as it
imports ``codeanalyzer-python`` and ``codeanalyzer-typescript`` for the other backends.

The analyzer reads its Java standard-library scope from the JVM it runs on (``jrt:/``),
so no ``JAVA_HOME`` and no ``jmods/`` are required.
"""

from __future__ import annotations

from importlib import resources
from pathlib import Path
from typing import List

__version__ = "0.0.0"

__all__ = ["jar_path", "java_path", "command", "__version__"]

_JAR_NAME = "codeanalyzer.jar"


def jar_path() -> Path:
"""Return the absolute path to the bundled ``codeanalyzer.jar``.

Raises:
FileNotFoundError: if the wheel did not include the JAR (a source/dev install
with no build run).
"""
resource = resources.files("codeanalyzer_java") / "_bin" / _JAR_NAME
with resources.as_file(resource) as extracted:
path = Path(extracted)
if not path.exists():
raise FileNotFoundError(
f"Bundled {_JAR_NAME} not found at {path}. Build it with `./gradlew fatJar` and "
"copy it there, or install the published wheel."
)
return path


def java_path() -> Path:
"""Return the ``java`` executable of the bundled runtime."""
import jdk4py

return Path(jdk4py.JAVA)


def command(*args: str) -> List[str]:
"""Return ``[java, -jar, codeanalyzer.jar, *args]`` ready for ``subprocess``."""
return [str(java_path()), "-jar", str(jar_path()), *args]
27 changes: 27 additions & 0 deletions packaging/python/src/codeanalyzer_java/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Console-script entry point: run the bundled analyzer on the bundled JVM.

``pip install codeanalyzer-java`` installs a ``canjv`` launcher (see ``[project.scripts]``
in ``pyproject.toml``) that calls :func:`main`, passing through every CLI argument and the
exit code unchanged.
"""

from __future__ import annotations

import os
import subprocess
import sys

from . import command


def main() -> "int | None":
argv = command(*sys.argv[1:])
if os.name == "posix":
# Replace this process with the JVM: no extra Python process lingers, and
# signals/exit codes are handled by the analyzer directly.
os.execv(argv[0], argv)
return subprocess.call(argv)


if __name__ == "__main__":
sys.exit(main())
5 changes: 5 additions & 0 deletions packaging/python/src/codeanalyzer_java/_bin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# The fat JAR is copied here at wheel-build time (see ../../build_wheel.sh) and
# bundled via `artifacts` in pyproject.toml. Never commit it. Keep the directory
# present so the package is importable in dev.
*
!.gitignore
Loading