From d360efa84d37e6d5539f51e80d23686f5cf6462a Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Fri, 23 Oct 2026 13:29:14 +0900 Subject: [PATCH 1/2] fix: leave the arcadedb-studio jar out of the wheels, Studio bundles ApexCharts (not open source since 5.1.0) Studio bundles ApexCharts, which left MIT after 5.0.0, and this package is Apache-2.0. arcadedb-studio-*.jar joins scripts/jar_exclusions.txt. scripts/verify_wheel_no_studio.py fails a wheel that contains a Studio jar, an ApexCharts file or a jar that carries one; build.sh runs it after the size check. test_server_packaging.py no longer requires the Studio jar and asserts that it and every ApexCharts file are absent. Co-Authored-By: Claude Sonnet 5.5 --- scripts/build.sh | 9 +++ scripts/jar_exclusions.txt | 5 ++ scripts/verify_wheel_no_studio.py | 92 +++++++++++++++++++++++++++ tests/test_server_packaging.py | 41 ++++++------ tests/test_wheel_no_studio.py | 102 ++++++++++++++++++++++++++++++ 5 files changed, 230 insertions(+), 19 deletions(-) create mode 100644 scripts/verify_wheel_no_studio.py create mode 100644 tests/test_wheel_no_studio.py diff --git a/scripts/build.sh b/scripts/build.sh index 4ac30bf4..19d56892 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -506,6 +506,15 @@ else "$SIZE_PY" "$SCRIPT_DIR/verify_wheel_size.py" "$NEWEST_WHEEL" || exit 1 fi +# THE LICENCE GUARD: Studio bundles ApexCharts, which is not open source since 5.1.0, and this package is Apache-2.0. +# A wheel with a Studio jar or an ApexCharts file fails here (https://github.com/ArcadeData/arcadedb/issues/10579). +if [[ -z "$SIZE_PY" ]]; then + echo -e "${RED}❌ No python found: the Studio/ApexCharts check cannot run${NC}" + exit 1 +else + "$SIZE_PY" "$SCRIPT_DIR/verify_wheel_no_studio.py" "$NEWEST_WHEEL" || exit 1 +fi + echo -e "${GREEN}🎉 Build completed successfully!${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" diff --git a/scripts/jar_exclusions.txt b/scripts/jar_exclusions.txt index 16fa7648..04b5827a 100644 --- a/scripts/jar_exclusions.txt +++ b/scripts/jar_exclusions.txt @@ -18,6 +18,11 @@ regex-*.jar arcadedb-ha-raft-*.jar arcadedb-metrics-*.jar arcadedb-mongodbw-*.jar +# Studio bundles ApexCharts, which has not been open source since 5.1.0 (Studio has shipped it since 26.1.1), and this +# package is Apache-2.0. Do not add this line back before upstream replaces the library: +# https://github.com/ArcadeData/arcadedb/issues/10579 . scripts/verify_wheel_no_studio.py fails a wheel that +# contains a Studio jar or an ApexCharts file anyway. +arcadedb-studio-*.jar # Embedded-unused (verified 2026-07-05 by quarantine + full suite/examples gate; # micrometer must STAY - server startup requires it): commons-math3-*.jar diff --git a/scripts/verify_wheel_no_studio.py b/scripts/verify_wheel_no_studio.py new file mode 100644 index 00000000..7afe69db --- /dev/null +++ b/scripts/verify_wheel_no_studio.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Fail a wheel that contains the Studio jar or an ApexCharts file. + +Studio bundles ApexCharts, which has not been open source since 5.1.0 (Community/Commercial licence; Studio has +shipped it since upstream 26.1.1). This package is Apache-2.0 and ships nothing that is not open source, so the wheels +leave the Studio jar out (`arcadedb-studio-*.jar` in scripts/jar_exclusions.txt). This check is the guard behind that +line: it fails when a Studio jar, any file named like ApexCharts, or a jar that carries ApexCharts files appears in a wheel, +whatever the cause (a removed exclusion, a shaded or renamed jar, a JAR directory that brings its own copy). +See https://github.com/ArcadeData/arcadedb/issues/10579. + +Usage: + python verify_wheel_no_studio.py WHEEL [WHEEL ...] + +Exit status: 0 all wheels clean, 1 a wheel contains Studio or ApexCharts, 2 usage error or not a wheel. +""" + +from __future__ import annotations + +import io +import sys +import zipfile +from pathlib import Path + +ISSUE_URL = "https://github.com/ArcadeData/arcadedb/issues/10579" + + +def _is_studio_jar(name: str) -> bool: + base = name.rsplit("/", 1)[-1].lower() + return base.startswith("arcadedb-studio") and base.endswith(".jar") + + +def _is_apexcharts(name: str) -> bool: + return "apexcharts" in name.lower() + + +def find_offenders(wheel: Path) -> list[str]: + """Names inside the wheel (and inside its jars) that are Studio or ApexCharts.""" + offenders: list[str] = [] + with zipfile.ZipFile(wheel) as zf: + for info in zf.infolist(): + name = info.filename + if _is_studio_jar(name): + offenders.append(f"{name} (Studio jar)") + elif _is_apexcharts(name): + offenders.append(f"{name} (ApexCharts file)") + elif name.lower().endswith(".jar"): + try: + inner = zipfile.ZipFile(io.BytesIO(zf.read(info))) + except zipfile.BadZipFile: + offenders.append( + f"{name} (jar that is not a zip file: cannot be checked)" + ) + continue + hits = [n for n in inner.namelist() if _is_apexcharts(n)] + if hits: + offenders.append( + f"{name} contains ApexCharts files, for example {hits[0]}" + ) + return offenders + + +def main(argv: list[str] | None = None) -> int: + wheels = [Path(a) for a in (sys.argv[1:] if argv is None else argv)] + if not wheels: + print(__doc__.strip().splitlines()[0], file=sys.stderr) + print("usage: verify_wheel_no_studio.py WHEEL [WHEEL ...]", file=sys.stderr) + return 2 + status = 0 + for wheel in wheels: + if not wheel.is_file() or not zipfile.is_zipfile(wheel): + print(f"ERROR: {wheel} is not a wheel file", file=sys.stderr) + return 2 + offenders = find_offenders(wheel) + if offenders: + status = 1 + print( + f"ERROR: {wheel.name} contains Studio or ApexCharts, which is not open source:", + file=sys.stderr, + ) + for entry in offenders: + print(f" {entry}", file=sys.stderr) + print( + f" Keep `arcadedb-studio-*.jar` in scripts/jar_exclusions.txt. Do not publish this wheel. {ISSUE_URL}", + file=sys.stderr, + ) + else: + print(f"OK: {wheel.name} has no Studio jar and no ApexCharts file") + return status + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_server_packaging.py b/tests/test_server_packaging.py index d1e0dabe..22c36505 100644 --- a/tests/test_server_packaging.py +++ b/tests/test_server_packaging.py @@ -24,10 +24,10 @@ # The JAR families that make server mode work. Sizes as measured on the # 26.8.1-SNAPSHOT image (uncompressed, MB) are in docs/guide/server.md; the -# whole set is ~7.65 MB uncompressed and ~7.3 MB on the wheel. +# whole set is ~7.65 MB uncompressed and ~7.3 MB on the wheel. Studio is +# deliberately NOT in this list: the wheels leave it out (test_studio_is_not_bundled). REQUIRED_JAR_PREFIXES = [ "arcadedb-server", # the server itself, 0.66 MB - "arcadedb-studio", # web UI assets, 2.60 MB, contains zero .class files "undertow-core", # HTTP server, 2.21 MB "xnio-api", # undertow's IO layer "xnio-nio", @@ -76,28 +76,31 @@ def test_server_api_is_importable_and_exported(): assert "create_server" in adb.__all__, "create_server missing from __all__" -def test_studio_jar_carries_no_classes(): - """Studio is static assets only, which is why bundling it is cheap. +def test_studio_is_not_bundled(): + """No Studio jar and no ApexCharts file in the installed package. - docs/guide/server.md tells users Studio costs disk and nothing else, - on the grounds that a JAR with no ``.class`` entries cannot execute. If a - future Studio release starts shipping code, that claim stops being true - and the documentation needs revisiting, so assert the premise rather than - trusting it. + Studio bundles ApexCharts, which has not been open source since 5.1.0, and this + package is Apache-2.0 (https://github.com/ArcadeData/arcadedb/issues/10579). + scripts/jar_exclusions.txt leaves the jar out and scripts/verify_wheel_no_studio.py + guards the wheel; this checks what pip actually installed. """ from arcadedb_embedded.jvm import get_jar_path jar_dir = get_jar_path() - studio = [n for n in os.listdir(jar_dir) if n.startswith("arcadedb-studio")] - assert studio, "studio JAR missing (see test_server_jars_are_bundled)" - - with zipfile.ZipFile(os.path.join(jar_dir, studio[0])) as z: - classes = [n for n in z.namelist() if n.endswith(".class")] - assert not classes, ( - f"arcadedb-studio now ships {len(classes)} .class files. " - f"docs/guide/server.md claims Studio costs disk only because it " - f"contains no executable code; update that claim." - ) + studio = [n for n in os.listdir(jar_dir) if n.lower().startswith("arcadedb-studio")] + assert not studio, f"Studio jar bundled again: {studio}" + + package_dir = os.path.dirname(jar_dir) + for root, _dirs, files in os.walk(package_dir): + for name in files: + path = os.path.join(root, name) + assert ( + "apexcharts" not in name.lower() + ), f"ApexCharts file in the wheel: {path}" + if name.endswith(".jar"): + with zipfile.ZipFile(path) as z: + hits = [n for n in z.namelist() if "apexcharts" in n.lower()] + assert not hits, f"{name} carries ApexCharts files: {hits[:3]}" @pytest.mark.server diff --git a/tests/test_wheel_no_studio.py b/tests/test_wheel_no_studio.py new file mode 100644 index 00000000..3b67144c --- /dev/null +++ b/tests/test_wheel_no_studio.py @@ -0,0 +1,102 @@ +"""The Studio/ApexCharts guard: a wheel that carries either fails the build. + +Studio bundles ApexCharts, which is not open source since 5.1.0 (ArcadeData/arcadedb#10579). The tests build tiny +wheels in a temporary directory, so they run in milliseconds and need no built wheel. +""" + +from __future__ import annotations + +import importlib.util +import io +import zipfile +from pathlib import Path + +SCRIPT_PATH = ( + Path(__file__).resolve().parents[1] / "scripts" / "verify_wheel_no_studio.py" +) +EXCLUSIONS = Path(__file__).resolve().parents[1] / "scripts" / "jar_exclusions.txt" + + +def _load(): + spec = importlib.util.spec_from_file_location("verify_wheel_no_studio", SCRIPT_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def _jar(members: dict[str, bytes]) -> bytes: + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as zf: + for name, data in members.items(): + zf.writestr(name, data) + return buf.getvalue() + + +def _wheel(path: Path, members: dict[str, bytes]) -> Path: + with zipfile.ZipFile(path, "w") as zf: + for name, data in members.items(): + zf.writestr(name, data) + return path + + +def test_studio_jar_is_in_the_exclusion_list(): + lines = [ln.strip() for ln in EXCLUSIONS.read_text().splitlines()] + assert "arcadedb-studio-*.jar" in lines + + +def test_a_clean_wheel_passes(tmp_path, capsys): + module = _load() + wheel = _wheel( + tmp_path / "a-1-py3-none-any.whl", + { + "arcadedb_embedded/jars/arcadedb-server-1.jar": _jar({"A.class": b"x"}), + "arcadedb_embedded/server.py": b"", + }, + ) + assert module.main([str(wheel)]) == 0 + assert "OK:" in capsys.readouterr().out + + +def test_a_studio_jar_fails_the_wheel(tmp_path, capsys): + module = _load() + wheel = _wheel( + tmp_path / "a-1-py3-none-any.whl", + { + "arcadedb_embedded/jars/arcadedb-studio-26.10.2.jar": _jar( + {"static/index.html": b"x"} + ) + }, + ) + assert module.main([str(wheel)]) == 1 + assert "Studio jar" in capsys.readouterr().err + + +def test_an_apexcharts_file_fails_the_wheel(tmp_path, capsys): + module = _load() + wheel = _wheel( + tmp_path / "a-1-py3-none-any.whl", + {"arcadedb_embedded/static/dist/js/apexcharts.min.js": b"x"}, + ) + assert module.main([str(wheel)]) == 1 + assert "ApexCharts file" in capsys.readouterr().err + + +def test_a_renamed_jar_that_carries_apexcharts_fails_the_wheel(tmp_path, capsys): + module = _load() + wheel = _wheel( + tmp_path / "a-1-py3-none-any.whl", + { + "arcadedb_embedded/jars/ui-1.jar": _jar( + {"static/dist/js/ApexCharts.min.js": b"x"} + ) + }, + ) + assert module.main([str(wheel), str(wheel)]) == 1 + assert "contains ApexCharts files" in capsys.readouterr().err + + +def test_a_missing_file_is_a_usage_error(tmp_path): + module = _load() + assert module.main([str(tmp_path / "nope.whl")]) == 2 + assert module.main([]) == 2 From e3a6a9aa8e714420495bd368e9b9f83a7055c38c Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Sun, 25 Oct 2026 01:14:51 +0900 Subject: [PATCH 2/2] fix: make the Studio guard a licence check; Studio ships from 26.11.1 ArcadeData/arcadedb#10627 replaced ApexCharts (not open source since 5.1.0) with Apache ECharts in Studio, first in engine 26.11.1, and this repository's first release is built from that engine. Leaving the Studio jar out of the wheels is no longer needed. - scripts/jar_exclusions.txt: drop `arcadedb-studio-*.jar`. - scripts/verify_wheel_no_studio.py becomes scripts/verify_wheel_licences.py: it fails a wheel with an ApexCharts file (in the wheel or inside any jar, so a Studio jar from an older engine is caught) and passes a Studio that carries ECharts. New products that are not open source go in its NON_OPEN_SOURCE table. scripts/build.sh runs it after the size check. - tests/test_wheel_no_studio.py becomes tests/test_wheel_licences.py: fake wheels with an echarts Studio (pass) and with apexcharts files (fail). - tests/test_server_packaging.py: Studio is a required jar again; test_no_apexcharts_in_the_package checks what pip installed. Co-Authored-By: Claude Sonnet 5.5 --- scripts/build.sh | 9 +- scripts/jar_exclusions.txt | 9 +- scripts/verify_wheel_licences.py | 105 ++++++++++++++++++ scripts/verify_wheel_no_studio.py | 92 --------------- tests/test_server_packaging.py | 43 +++++-- ...el_no_studio.py => test_wheel_licences.py} | 73 +++++++++--- 6 files changed, 205 insertions(+), 126 deletions(-) create mode 100644 scripts/verify_wheel_licences.py delete mode 100644 scripts/verify_wheel_no_studio.py rename tests/{test_wheel_no_studio.py => test_wheel_licences.py} (52%) diff --git a/scripts/build.sh b/scripts/build.sh index 4a7a7732..30329914 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -522,13 +522,14 @@ else "$SIZE_PY" "$SCRIPT_DIR/verify_wheel_size.py" "$NEWEST_WHEEL" || exit 1 fi -# THE LICENCE GUARD: Studio bundles ApexCharts, which is not open source since 5.1.0, and this package is Apache-2.0. -# A wheel with a Studio jar or an ApexCharts file fails here (https://github.com/ArcadeData/arcadedb/issues/10579). +# THE LICENCE GUARD: this package is Apache-2.0 and ships only open source. Studio bundled ApexCharts (not open +# source since 5.1.0) until engine 26.11.1 replaced it with ECharts (ArcadeData/arcadedb#10627). A wheel with an +# ApexCharts file fails here (https://github.com/ArcadeData/arcadedb/issues/10579). if [[ -z "$SIZE_PY" ]]; then - echo -e "${RED}❌ No python found: the Studio/ApexCharts check cannot run${NC}" + echo -e "${RED}❌ No python found: the licence check cannot run${NC}" exit 1 else - "$SIZE_PY" "$SCRIPT_DIR/verify_wheel_no_studio.py" "$NEWEST_WHEEL" || exit 1 + "$SIZE_PY" "$SCRIPT_DIR/verify_wheel_licences.py" "$NEWEST_WHEEL" || exit 1 fi echo -e "${GREEN}🎉 Build completed successfully!${NC}" diff --git a/scripts/jar_exclusions.txt b/scripts/jar_exclusions.txt index f77671bd..e959a09f 100644 --- a/scripts/jar_exclusions.txt +++ b/scripts/jar_exclusions.txt @@ -18,11 +18,6 @@ regex-*.jar arcadedb-ha-raft-*.jar arcadedb-metrics-*.jar arcadedb-mongodbw-*.jar -# Studio bundles ApexCharts, which has not been open source since 5.1.0 (Studio has shipped it since 26.1.1), and this -# package is Apache-2.0. Do not add this line back before upstream replaces the library: -# https://github.com/ArcadeData/arcadedb/issues/10579 . scripts/verify_wheel_no_studio.py fails a wheel that -# contains a Studio jar or an ApexCharts file anyway. -arcadedb-studio-*.jar # Verified 2026-07-05 by quarantine + full suite/examples gate; micrometer must STAY # (server startup requires it): arcadedb-tracing-*.jar @@ -35,3 +30,7 @@ jline-*.jar # snappy-java-*.jar CompressedBodyDecoder and the Prometheus remote read and # write routes # commons-math3-*.jar jvector's ScoreTracker$TwoPhaseTracker +# arcadedb-studio-*.jar the Studio web UI. It bundled ApexCharts (not open source since 5.1.0) until +# ArcadeData/arcadedb#10627 replaced it with Apache ECharts, first in engine 26.11.1. +# scripts/verify_wheel_licences.py fails a wheel that carries ApexCharts files, so a +# wheel built from an older engine cannot ship. diff --git a/scripts/verify_wheel_licences.py b/scripts/verify_wheel_licences.py new file mode 100644 index 00000000..9677775a --- /dev/null +++ b/scripts/verify_wheel_licences.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Fail a wheel that contains a file that is not open source. + +This package is Apache-2.0 and ships nothing that is not open source. One known case today: ApexCharts, which has not +been open source since 5.1.0 (Community/Commercial licence). Studio, the web UI in `arcadedb-studio-*.jar`, bundled it +from upstream 26.1.1 until ArcadeData/arcadedb#10627 replaced it with Apache ECharts (first in engine 26.11.1). Studio +ships in the wheels again, so this check does not look for the Studio jar. It looks for the files that make a wheel +unfit to publish, whatever the cause: a wheel built from an engine older than 26.11.1, a shaded or renamed jar, a JAR +directory that brings its own copy. A wheel whose Studio carries ECharts passes. +See https://github.com/ArcadeData/arcadedb/issues/10579 and https://github.com/ArcadeData/arcadedb/pull/10627. + +To add a case, add a row to NON_OPEN_SOURCE: a lower-case marker that appears in the path of every file of that product, +and a short description for the error message. + +Usage: + python verify_wheel_licences.py WHEEL [WHEEL ...] + +Exit status: 0 all wheels clean, 1 a wheel contains a file that is not open source, 2 usage error or not a wheel. +""" + +from __future__ import annotations + +import io +import sys +import zipfile +from pathlib import Path + +# (marker in the lower-cased path, description). A file matches when its path, or the path of an entry inside a jar, +# contains the marker. +NON_OPEN_SOURCE: tuple[tuple[str, str], ...] = ( + ( + "apexcharts", + "ApexCharts, not open source since 5.1.0 (Studio in engines before 26.11.1)", + ), +) + +HELP = ( + "Do not publish this wheel. Studio uses Apache ECharts from engine 26.11.1 " + "(https://github.com/ArcadeData/arcadedb/pull/10627): build from the 26.11.1 jars or newer " + "(see https://github.com/ArcadeData/arcadedb/issues/10579)." +) + + +def _match(path: str) -> str | None: + """The description of the non-open-source product the path belongs to, else None.""" + lowered = path.lower() + for marker, description in NON_OPEN_SOURCE: + if marker in lowered: + return description + return None + + +def find_offenders(wheel: Path) -> list[str]: + """Names inside the wheel (and inside its jars) that belong to a product that is not open source.""" + offenders: list[str] = [] + with zipfile.ZipFile(wheel) as zf: + for info in zf.infolist(): + name = info.filename + description = _match(name) + if description is not None: + offenders.append(f"{name} ({description})") + elif name.lower().endswith(".jar"): + try: + inner = zipfile.ZipFile(io.BytesIO(zf.read(info))) + except zipfile.BadZipFile: + offenders.append( + f"{name} (jar that is not a zip file: cannot be checked)" + ) + continue + for entry in inner.namelist(): + description = _match(entry) + if description is not None: + offenders.append(f"{name} contains {entry} ({description})") + break + return offenders + + +def main(argv: list[str] | None = None) -> int: + wheels = [Path(a) for a in (sys.argv[1:] if argv is None else argv)] + if not wheels: + print(__doc__.strip().splitlines()[0], file=sys.stderr) + print("usage: verify_wheel_licences.py WHEEL [WHEEL ...]", file=sys.stderr) + return 2 + status = 0 + for wheel in wheels: + if not wheel.is_file() or not zipfile.is_zipfile(wheel): + print(f"ERROR: {wheel} is not a wheel file", file=sys.stderr) + return 2 + offenders = find_offenders(wheel) + if offenders: + status = 1 + print( + f"ERROR: {wheel.name} contains files that are not open source:", + file=sys.stderr, + ) + for entry in offenders: + print(f" {entry}", file=sys.stderr) + print(f" {HELP}", file=sys.stderr) + else: + print(f"OK: {wheel.name} has no file that is not open source") + return status + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/verify_wheel_no_studio.py b/scripts/verify_wheel_no_studio.py deleted file mode 100644 index 7afe69db..00000000 --- a/scripts/verify_wheel_no_studio.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -"""Fail a wheel that contains the Studio jar or an ApexCharts file. - -Studio bundles ApexCharts, which has not been open source since 5.1.0 (Community/Commercial licence; Studio has -shipped it since upstream 26.1.1). This package is Apache-2.0 and ships nothing that is not open source, so the wheels -leave the Studio jar out (`arcadedb-studio-*.jar` in scripts/jar_exclusions.txt). This check is the guard behind that -line: it fails when a Studio jar, any file named like ApexCharts, or a jar that carries ApexCharts files appears in a wheel, -whatever the cause (a removed exclusion, a shaded or renamed jar, a JAR directory that brings its own copy). -See https://github.com/ArcadeData/arcadedb/issues/10579. - -Usage: - python verify_wheel_no_studio.py WHEEL [WHEEL ...] - -Exit status: 0 all wheels clean, 1 a wheel contains Studio or ApexCharts, 2 usage error or not a wheel. -""" - -from __future__ import annotations - -import io -import sys -import zipfile -from pathlib import Path - -ISSUE_URL = "https://github.com/ArcadeData/arcadedb/issues/10579" - - -def _is_studio_jar(name: str) -> bool: - base = name.rsplit("/", 1)[-1].lower() - return base.startswith("arcadedb-studio") and base.endswith(".jar") - - -def _is_apexcharts(name: str) -> bool: - return "apexcharts" in name.lower() - - -def find_offenders(wheel: Path) -> list[str]: - """Names inside the wheel (and inside its jars) that are Studio or ApexCharts.""" - offenders: list[str] = [] - with zipfile.ZipFile(wheel) as zf: - for info in zf.infolist(): - name = info.filename - if _is_studio_jar(name): - offenders.append(f"{name} (Studio jar)") - elif _is_apexcharts(name): - offenders.append(f"{name} (ApexCharts file)") - elif name.lower().endswith(".jar"): - try: - inner = zipfile.ZipFile(io.BytesIO(zf.read(info))) - except zipfile.BadZipFile: - offenders.append( - f"{name} (jar that is not a zip file: cannot be checked)" - ) - continue - hits = [n for n in inner.namelist() if _is_apexcharts(n)] - if hits: - offenders.append( - f"{name} contains ApexCharts files, for example {hits[0]}" - ) - return offenders - - -def main(argv: list[str] | None = None) -> int: - wheels = [Path(a) for a in (sys.argv[1:] if argv is None else argv)] - if not wheels: - print(__doc__.strip().splitlines()[0], file=sys.stderr) - print("usage: verify_wheel_no_studio.py WHEEL [WHEEL ...]", file=sys.stderr) - return 2 - status = 0 - for wheel in wheels: - if not wheel.is_file() or not zipfile.is_zipfile(wheel): - print(f"ERROR: {wheel} is not a wheel file", file=sys.stderr) - return 2 - offenders = find_offenders(wheel) - if offenders: - status = 1 - print( - f"ERROR: {wheel.name} contains Studio or ApexCharts, which is not open source:", - file=sys.stderr, - ) - for entry in offenders: - print(f" {entry}", file=sys.stderr) - print( - f" Keep `arcadedb-studio-*.jar` in scripts/jar_exclusions.txt. Do not publish this wheel. {ISSUE_URL}", - file=sys.stderr, - ) - else: - print(f"OK: {wheel.name} has no Studio jar and no ApexCharts file") - return status - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/test_server_packaging.py b/tests/test_server_packaging.py index f97637e7..ddc8f6ef 100644 --- a/tests/test_server_packaging.py +++ b/tests/test_server_packaging.py @@ -24,10 +24,10 @@ # The JAR families that make server mode work. Sizes as measured on the # 26.8.1-SNAPSHOT image (uncompressed, MB) are in docs/guide/server.md; the -# whole set is ~7.65 MB uncompressed and ~7.3 MB on the wheel. Studio is -# deliberately NOT in this list: the wheels leave it out (test_studio_is_not_bundled). +# whole set is ~7.65 MB uncompressed and ~7.3 MB on the wheel. REQUIRED_JAR_PREFIXES = [ "arcadedb-server", # the server itself, 0.66 MB + "arcadedb-studio", # web UI assets, 2.60 MB, contains zero .class files "undertow-core", # HTTP server, 2.21 MB "xnio-api", # undertow's IO layer "xnio-nio", @@ -76,21 +76,42 @@ def test_server_api_is_importable_and_exported(): assert "create_server" in adb.__all__, "create_server missing from __all__" -def test_studio_is_not_bundled(): - """No Studio jar and no ApexCharts file in the installed package. +def test_studio_jar_carries_no_classes(): + """Studio is static assets only, which is why bundling it is cheap. - Studio bundles ApexCharts, which has not been open source since 5.1.0, and this - package is Apache-2.0 (https://github.com/ArcadeData/arcadedb/issues/10579). - scripts/jar_exclusions.txt leaves the jar out and scripts/verify_wheel_no_studio.py - guards the wheel; this checks what pip actually installed. + docs/guide/server.md tells users Studio costs disk and nothing else, + on the grounds that a JAR with no ``.class`` entries cannot execute. If a + future Studio release starts shipping code, that claim stops being true + and the documentation needs revisiting, so assert the premise rather than + trusting it. """ from arcadedb_embedded.jvm import get_jar_path jar_dir = get_jar_path() - studio = [n for n in os.listdir(jar_dir) if n.lower().startswith("arcadedb-studio")] - assert not studio, f"Studio jar bundled again: {studio}" + studio = [n for n in os.listdir(jar_dir) if n.startswith("arcadedb-studio")] + assert studio, "studio JAR missing (see test_server_jars_are_bundled)" + + with zipfile.ZipFile(os.path.join(jar_dir, studio[0])) as z: + classes = [n for n in z.namelist() if n.endswith(".class")] + assert not classes, ( + f"arcadedb-studio now ships {len(classes)} .class files. " + f"docs/guide/server.md claims Studio costs disk only because it " + f"contains no executable code; update that claim." + ) + + +def test_no_apexcharts_in_the_package(): + """No ApexCharts file in the installed package, in or outside a jar. + + Studio bundled ApexCharts, which has not been open source since 5.1.0, until + engine 26.11.1 replaced it with Apache ECharts (ArcadeData/arcadedb#10627). This + package is Apache-2.0, so a Studio jar from an older engine must not be installed. + scripts/verify_wheel_licences.py guards the wheel at build time; this checks what + pip actually installed. + """ + from arcadedb_embedded.jvm import get_jar_path - package_dir = os.path.dirname(jar_dir) + package_dir = os.path.dirname(get_jar_path()) for root, _dirs, files in os.walk(package_dir): for name in files: path = os.path.join(root, name) diff --git a/tests/test_wheel_no_studio.py b/tests/test_wheel_licences.py similarity index 52% rename from tests/test_wheel_no_studio.py rename to tests/test_wheel_licences.py index 3b67144c..d5030f9b 100644 --- a/tests/test_wheel_no_studio.py +++ b/tests/test_wheel_licences.py @@ -1,7 +1,8 @@ -"""The Studio/ApexCharts guard: a wheel that carries either fails the build. +"""The licence guard: a wheel that carries a file that is not open source fails the build. -Studio bundles ApexCharts, which is not open source since 5.1.0 (ArcadeData/arcadedb#10579). The tests build tiny -wheels in a temporary directory, so they run in milliseconds and need no built wheel. +Studio bundled ApexCharts, which is not open source since 5.1.0 (ArcadeData/arcadedb#10579), until engine 26.11.1 +replaced it with Apache ECharts (ArcadeData/arcadedb#10627). The tests build tiny wheels in a temporary directory, so +they run in milliseconds and need no built wheel. """ from __future__ import annotations @@ -12,13 +13,13 @@ from pathlib import Path SCRIPT_PATH = ( - Path(__file__).resolve().parents[1] / "scripts" / "verify_wheel_no_studio.py" + Path(__file__).resolve().parents[1] / "scripts" / "verify_wheel_licences.py" ) EXCLUSIONS = Path(__file__).resolve().parents[1] / "scripts" / "jar_exclusions.txt" def _load(): - spec = importlib.util.spec_from_file_location("verify_wheel_no_studio", SCRIPT_PATH) + spec = importlib.util.spec_from_file_location("verify_wheel_licences", SCRIPT_PATH) module = importlib.util.module_from_spec(spec) assert spec.loader is not None spec.loader.exec_module(module) @@ -40,9 +41,10 @@ def _wheel(path: Path, members: dict[str, bytes]) -> Path: return path -def test_studio_jar_is_in_the_exclusion_list(): +def test_studio_jar_is_not_in_the_exclusion_list(): + # Studio ships from 26.11.1 (ECharts); the guard, not an exclusion, keeps ApexCharts out. lines = [ln.strip() for ln in EXCLUSIONS.read_text().splitlines()] - assert "arcadedb-studio-*.jar" in lines + assert not [ln for ln in lines if not ln.startswith("#") and "studio" in ln] def test_a_clean_wheel_passes(tmp_path, capsys): @@ -58,18 +60,30 @@ def test_a_clean_wheel_passes(tmp_path, capsys): assert "OK:" in capsys.readouterr().out -def test_a_studio_jar_fails_the_wheel(tmp_path, capsys): +def test_a_studio_jar_with_echarts_passes(tmp_path, capsys): module = _load() wheel = _wheel( tmp_path / "a-1-py3-none-any.whl", { - "arcadedb_embedded/jars/arcadedb-studio-26.10.2.jar": _jar( - {"static/index.html": b"x"} + "arcadedb_embedded/jars/arcadedb-studio-26.11.1.jar": _jar( + { + "static/index.html": b"x", + "static/dist/js/echarts.min.js": b"x", + } ) }, ) - assert module.main([str(wheel)]) == 1 - assert "Studio jar" in capsys.readouterr().err + assert module.main([str(wheel)]) == 0 + assert "OK:" in capsys.readouterr().out + + +def test_an_echarts_file_outside_a_jar_passes(tmp_path): + module = _load() + wheel = _wheel( + tmp_path / "a-1-py3-none-any.whl", + {"arcadedb_embedded/static/dist/js/echarts.min.js": b"x"}, + ) + assert module.main([str(wheel)]) == 0 def test_an_apexcharts_file_fails_the_wheel(tmp_path, capsys): @@ -79,7 +93,28 @@ def test_an_apexcharts_file_fails_the_wheel(tmp_path, capsys): {"arcadedb_embedded/static/dist/js/apexcharts.min.js": b"x"}, ) assert module.main([str(wheel)]) == 1 - assert "ApexCharts file" in capsys.readouterr().err + err = capsys.readouterr().err + assert "ApexCharts" in err + assert "apexcharts.min.js" in err + + +def test_a_studio_jar_from_an_older_engine_fails_the_wheel(tmp_path, capsys): + module = _load() + wheel = _wheel( + tmp_path / "a-1-py3-none-any.whl", + { + "arcadedb_embedded/jars/arcadedb-studio-26.10.2.jar": _jar( + { + "static/index.html": b"x", + "static/dist/js/apexcharts.min.js": b"x", + } + ) + }, + ) + assert module.main([str(wheel)]) == 1 + err = capsys.readouterr().err + assert "arcadedb-studio-26.10.2.jar contains" in err + assert "26.11.1" in err def test_a_renamed_jar_that_carries_apexcharts_fails_the_wheel(tmp_path, capsys): @@ -93,7 +128,17 @@ def test_a_renamed_jar_that_carries_apexcharts_fails_the_wheel(tmp_path, capsys) }, ) assert module.main([str(wheel), str(wheel)]) == 1 - assert "contains ApexCharts files" in capsys.readouterr().err + assert "ui-1.jar contains" in capsys.readouterr().err + + +def test_a_jar_that_is_not_a_zip_fails_the_wheel(tmp_path, capsys): + module = _load() + wheel = _wheel( + tmp_path / "a-1-py3-none-any.whl", + {"arcadedb_embedded/jars/broken-1.jar": b"not a zip"}, + ) + assert module.main([str(wheel)]) == 1 + assert "cannot be checked" in capsys.readouterr().err def test_a_missing_file_is_a_usage_error(tmp_path):