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
17 changes: 14 additions & 3 deletions frontend/service/studio_release_server/offline_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Build the locked Linux wheelhouse consumed by VeFaaS Studio releases."""
"""Build the locked Linux wheel set consumed by VeFaaS Studio releases."""

from __future__ import annotations

Expand Down Expand Up @@ -193,9 +193,20 @@ def build_studio_offline_runtime(
if not staged_veadk.is_file():
raise ValueError("Studio offline wheelhouse is incomplete.")
_pin_runtime_lock_to_wheelhouse(runtime_lock, wheelhouse, staged_veadk)
for wheel in sorted(wheelhouse.glob("*.whl")):
destination = package_dir / wheel.name
if destination.exists():
raise ValueError("Studio offline wheel has a root-level conflict.")
shutil.move(str(wheel), destination)
try:
wheelhouse.rmdir()
except OSError as error:
raise ValueError(
"Studio offline wheelhouse contains unexpected files."
) from error
requirements = build_studio_offline_requirements(
wheelhouse,
wheel_prefix=f"./{STUDIO_RUNTIME_WHEELHOUSE}/",
package_dir,
wheel_prefix="./",
)
_verify_offline_resolution(
package_dir,
Expand Down
41 changes: 28 additions & 13 deletions frontend/service/studio_release_server/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@
import tomli as tomllib # pyright: ignore[reportMissingImports]

if __package__:
from .offline_runtime import build_studio_offline_runtime
from .offline_runtime import (
build_studio_offline_requirements,
build_studio_offline_runtime,
)
else:
_offline_runtime_path = Path(__file__).with_name("offline_runtime.py")
_offline_runtime_spec = importlib.util.spec_from_file_location(
Expand All @@ -56,6 +59,9 @@
raise RuntimeError("Studio offline runtime builder is unavailable.")
_offline_runtime = importlib.util.module_from_spec(_offline_runtime_spec)
_offline_runtime_spec.loader.exec_module(_offline_runtime)
build_studio_offline_requirements = (
_offline_runtime.build_studio_offline_requirements
)
build_studio_offline_runtime = _offline_runtime.build_studio_offline_runtime

_VERSION_PATTERN = re.compile(r"^\d{14}$")
Expand Down Expand Up @@ -917,6 +923,23 @@ def validate_studio_bundle_dependencies(package_dir: Path) -> Path:
raise StudioPublisherError(
"The Studio release must contain exactly one local VeADK wheel."
)
try:
expected_requirements = build_studio_offline_requirements(
package_dir,
wheel_prefix="./",
)
except ValueError as error:
raise StudioPublisherError(
"Studio full release dependency contract is invalid."
) from error
if (
(package_dir / "wheelhouse").exists()
or requirements_path.read_text(encoding="utf-8") != expected_requirements
or set(local_wheels) != {path.resolve() for path in package_dir.glob("*.whl")}
):
raise StudioPublisherError(
"Studio full release dependency contract is invalid."
)
return validate_studio_agentkit_cli_archive(list(package_dir.iterdir()))


Expand Down Expand Up @@ -1023,25 +1046,19 @@ def stage_studio_thin_runtime(
"""Replace local runtime payloads with one exact public artifact manifest."""

contract = _load_studio_artifact_contract(source_root)
wheelhouse = package_dir / "wheelhouse"
wheels = sorted(wheelhouse.glob("*.whl"))
wheels = sorted(package_dir.glob("*.whl"))
runtime_veadk_wheels = [
path
for path in wheels
if path.name.startswith(("veadk_python-", "veadk-python-"))
]
dependency_wheels = [path for path in wheels if path not in runtime_veadk_wheels]
local_veadk_wheels = sorted(package_dir.glob("veadk*.whl"))
if len(runtime_veadk_wheels) == 1 and not local_veadk_wheels:
local_veadk = package_dir / runtime_veadk_wheels[0].name
shutil.copy2(runtime_veadk_wheels[0], local_veadk)
local_veadk_wheels = [local_veadk]
local_veadk_wheels = runtime_veadk_wheels
cli_archive = package_dir / _AGENTKIT_CLI_ARCHIVE
if (
not dependency_wheels
or len(runtime_veadk_wheels) != 1
or len(local_veadk_wheels) != 1
or _sha256_file(runtime_veadk_wheels[0]) != _sha256_file(local_veadk_wheels[0])
or not cli_archive.is_file()
):
raise StudioPublisherError("Studio offline runtime is incomplete.")
Expand Down Expand Up @@ -1075,12 +1092,12 @@ def stage_studio_thin_runtime(
artifact_dir = output_dir / f"runtime-artifacts-{runtime_manifest.runtime_epoch}"
artifact_dir.mkdir(parents=True, exist_ok=False)
for path in (*public_wheels, cli_archive):
shutil.copy2(path, artifact_dir / path.name)
shutil.move(str(path), artifact_dir / path.name)
if bundled_wheels:
bundled_wheelhouse = package_dir / "bundled-wheelhouse"
bundled_wheelhouse.mkdir()
for path in bundled_wheels:
shutil.copy2(path, bundled_wheelhouse / path.name)
shutil.move(str(path), bundled_wheelhouse / path.name)
manifest_content = runtime_manifest.to_json()
(package_dir / _STUDIO_RUNTIME_MANIFEST).write_bytes(manifest_content)
(
Expand All @@ -1092,8 +1109,6 @@ def stage_studio_thin_runtime(
+ f"--hash=sha256:{_sha256_file(local_veadk_wheels[0])}\n",
encoding="utf-8",
)
shutil.rmtree(wheelhouse)
cli_archive.unlink()
(package_dir / "run.sh").write_text(
_studio_run_script(thin=True),
encoding="utf-8",
Expand Down
16 changes: 4 additions & 12 deletions tests/cli/test_studio_deploy_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,13 @@ def _build_test_offline_runtime(
veadk_wheel: Path,
**_kwargs: object,
) -> str:
wheelhouse = package_dir / "wheelhouse"
wheelhouse.mkdir()
target = wheelhouse / veadk_wheel.name
target = package_dir / veadk_wheel.name
target.write_bytes(veadk_wheel.read_bytes())
(package_dir / "studio-runtime.lock").write_text(
"dependency==1\n",
encoding="utf-8",
)
return (
"--no-index\n"
"--find-links ./wheelhouse\n"
"-r ./studio-runtime.lock\n"
f"./wheelhouse/{target.name}\n"
)
return f"--no-index\n--require-hashes\n./{target.name} --hash=sha256:test\n"


def _stage_test_agentkit_cli_archive(
Expand Down Expand Up @@ -2445,9 +2438,8 @@ def _fake_build(command: list[str], check: bool) -> None:
assert expected_provider in {"volcengine", "byteplus"}
expected_requirements = (
"--no-index\n"
"--find-links ./wheelhouse\n"
"-r ./studio-runtime.lock\n"
"./wheelhouse/veadk_python-test-py3-none-any.whl\n"
"--require-hashes\n"
"./veadk_python-test-py3-none-any.whl --hash=sha256:test\n"
)
assert captured["requirements"] == expected_requirements

Expand Down
34 changes: 28 additions & 6 deletions tests/cli/test_studio_offline_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ def _write_pure_python_wheel(path: Path, *, name: str, version: str) -> None:
wheel.writestr(f"{dist_info}/RECORD", "")


def _stage_root_only_updater(package_root: Path, destination: Path) -> None:
"""Mirror the oldest released Scheduler staging contract exactly."""
requirements = package_root / "requirements.txt"
shutil.copy2(requirements, destination / requirements.name)
for wheel in package_root.glob("*.whl"):
shutil.copy2(wheel, destination / wheel.name)


def test_lock_check_environment_uses_canonical_pypi() -> None:
environment = offline_runtime._lock_check_environment(
{
Expand Down Expand Up @@ -232,10 +240,10 @@ def fake_run(command: list[str], **_kwargs: object) -> subprocess.CompletedProce
environment={"PATH": "/usr/bin"},
)

staged_veadk = package_dir / "wheelhouse" / veadk_wheel.name
expected_wheels = sorted((package_dir / "wheelhouse").glob("*.whl"))
staged_veadk = package_dir / veadk_wheel.name
expected_wheels = sorted(package_dir.glob("*.whl"))
assert requirements == "--no-index\n--require-hashes\n" + "".join(
f"./wheelhouse/{wheel.name} --hash=sha256:{offline_runtime._sha256(wheel)}\n"
f"./{wheel.name} --hash=sha256:{offline_runtime._sha256(wheel)}\n"
for wheel in expected_wheels
)
assert "--find-links" not in requirements
Expand All @@ -255,21 +263,35 @@ def fake_run(command: list[str], **_kwargs: object) -> subprocess.CompletedProce
assert verification[1:3] == ["pip", "install"]
assert verification[-2:] == ["--requirements", "-"]

historical_stage = tmp_path / "historical-scheduler"
historical_stage.mkdir()
(package_dir / "requirements.txt").write_text(requirements, encoding="utf-8")
_stage_root_only_updater(package_dir, historical_stage)
assert sorted(path.name for path in historical_stage.glob("*.whl")) == sorted(
path.name for path in expected_wheels
)

platform_parse = original_run(
[
uv,
"pip",
"install",
"--dry-run",
"--no-deps",
"--offline",
"--no-index",
"--require-hashes",
"--no-python-downloads",
"--python-version",
"3.12",
"--python-platform",
"x86_64-manylinux_2_28",
"--target",
str(tmp_path / "platform-target"),
"--requirements",
"-",
],
cwd=package_dir,
input=requirements,
cwd=historical_stage,
input=(historical_stage / "requirements.txt").read_text(encoding="utf-8"),
text=True,
capture_output=True,
check=False,
Expand Down
26 changes: 26 additions & 0 deletions tests/cli/test_studio_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io
import json
import os
import shutil
import subprocess
import sys
import zipfile
Expand Down Expand Up @@ -614,13 +615,38 @@ def test_extracted_bundle_requires_local_pinned_archive(
package = tmp_path / "package"
veadk_wheel, cli_archive = _write_release_package(package, monkeypatch)
(package / "requirements.txt").write_text(
"--no-index\n"
"--require-hashes\n"
f"./{veadk_wheel.name} --hash=sha256:{hashlib.sha256(veadk_wheel.read_bytes()).hexdigest()}\n",
encoding="utf-8",
)

assert validate_studio_bundle_dependencies(package) == cli_archive


def test_extracted_bundle_rejects_nested_wheelhouse_contract(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
package = tmp_path / "package"
wheelhouse = package / "wheelhouse"
veadk_wheel, _cli_archive = _write_release_package(wheelhouse, monkeypatch)
shutil.move(
str(wheelhouse / STUDIO_AGENTKIT_CLI_ARTIFACT.filename),
package / STUDIO_AGENTKIT_CLI_ARTIFACT.filename,
)
(package / "requirements.txt").write_text(
"--no-index\n"
"--require-hashes\n"
f"./wheelhouse/{veadk_wheel.name} "
f"--hash=sha256:{hashlib.sha256(veadk_wheel.read_bytes()).hexdigest()}\n",
encoding="utf-8",
)

with pytest.raises(StudioPublisherError, match="full release dependency"):
validate_studio_bundle_dependencies(package)


def test_release_entrypoint_reads_deployed_provider() -> None:
run_script = studio_run_script(provider=None)

Expand Down
Loading
Loading