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
10 changes: 10 additions & 0 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,16 @@ else
"$SIZE_PY" "$SCRIPT_DIR/verify_wheel_size.py" "$NEWEST_WHEEL" || exit 1
fi

# 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 licence check cannot run${NC}"
exit 1
else
"$SIZE_PY" "$SCRIPT_DIR/verify_wheel_licences.py" "$NEWEST_WHEEL" || exit 1
fi

echo -e "${GREEN}🎉 Build completed successfully!${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
Expand Down
4 changes: 4 additions & 0 deletions scripts/jar_exclusions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,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.
105 changes: 105 additions & 0 deletions scripts/verify_wheel_licences.py
Original file line number Diff line number Diff line change
@@ -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())
24 changes: 24 additions & 0 deletions tests/test_server_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,30 @@ def test_studio_jar_carries_no_classes():
)


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(get_jar_path())
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
def test_server_starts_and_serves_http(temp_server_root):
"""End-to-end: the bundled stack actually starts and answers.
Expand Down
147 changes: 147 additions & 0 deletions tests/test_wheel_licences.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""The licence guard: a wheel that carries a file that is not open source fails the build.

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

import importlib.util
import io
import zipfile
from pathlib import Path

SCRIPT_PATH = (
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_licences", 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_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 not [ln for ln in lines if not ln.startswith("#") and "studio" in ln]


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_with_echarts_passes(tmp_path, capsys):
module = _load()
wheel = _wheel(
tmp_path / "a-1-py3-none-any.whl",
{
"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)]) == 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):
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
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):
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 "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):
module = _load()
assert module.main([str(tmp_path / "nope.whl")]) == 2
assert module.main([]) == 2
Loading