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
22 changes: 20 additions & 2 deletions scripts/stage-package-repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,23 @@ def _load_locked_public_key(
return public_key_data


def _resolve_executable(path: Path, context: str) -> Path:
candidate = path
if not candidate.is_absolute() and len(candidate.parts) == 1:
located = shutil.which(str(candidate))
if located is None:
fail(f"unable to locate {context}")
candidate = Path(located)
try:
resolved = candidate.resolve(strict=True)
except OSError as error:
fail(f"unable to canonicalize {context}: {error}")
_regular_file(resolved, context)
if not os.access(resolved, os.X_OK):
fail(f"{context} is not executable")
return resolved


def _verify_signed_index(
apk_executable: Path,
repository: Path,
Expand Down Expand Up @@ -425,6 +442,7 @@ def _archive_repository(source: Path, archive: Path, repository_directory: str)

def command_build(args: argparse.Namespace) -> None:
config = repository_config(args.lock)
apk_executable = _resolve_executable(args.apk_executable, "apk executable")
input_dir = _real_directory(args.input_dir, "APK input directory")
output_dir = _safe_existing_parent(args.output_dir, "repository output directory")
archive = _safe_existing_parent(args.archive, "repository archive")
Expand Down Expand Up @@ -460,7 +478,7 @@ def command_build(args: argparse.Namespace) -> None:
digest, size = copy_regular(source, temporary / source.name, config["max_member_bytes"])
package_records.append({"filename": source.name, "sha256": digest, "size": size})
command = [
str(args.apk_executable),
str(apk_executable),
"mkndx",
"--allow-untrusted",
"--sign",
Expand Down Expand Up @@ -491,7 +509,7 @@ def command_build(args: argparse.Namespace) -> None:
index_path = temporary / "packages.adb"
_regular_file(index_path, "packages.adb", config["max_member_bytes"])
_verify_signed_index(
args.apk_executable,
apk_executable,
temporary,
public_key_data,
args.apk_timeout,
Expand Down
42 changes: 40 additions & 2 deletions tests/test_package_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def run_script(
*arguments: object,
expected: int = 0,
extra_env: dict[str, str] | None = None,
cwd: Path | None = None,
) -> subprocess.CompletedProcess[str]:
command = [sys.executable, str(SCRIPT), "--lock", str(self.lock), *(str(item) for item in arguments)]
environment = os.environ.copy()
Expand All @@ -126,7 +127,7 @@ def run_script(
)
if extra_env:
environment.update(extra_env)
result = subprocess.run(command, text=True, capture_output=True, check=False, env=environment)
result = subprocess.run(command, text=True, capture_output=True, check=False, env=environment, cwd=cwd)
self.assertEqual(
result.returncode,
expected,
Expand All @@ -140,8 +141,10 @@ def build(
self,
*,
public_key: Path | None = None,
apk_executable: Path | None = None,
expected: int = 0,
extra_env: dict[str, str] | None = None,
cwd: Path | None = None,
) -> tuple[Path, Path, subprocess.CompletedProcess[str]]:
output = self.root / "repository"
archive = self.root / self.asset_name
Expand All @@ -158,9 +161,10 @@ def build(
"--public-key",
public_key or self.public_key,
"--apk-executable",
self.fake_apk,
apk_executable or self.fake_apk,
expected=expected,
extra_env=extra_env,
cwd=cwd,
)
return output, archive, result

Expand Down Expand Up @@ -252,6 +256,40 @@ def test_successful_build_verifies_signature_and_pages_stage(self) -> None:
staged = self.root / "site/packages" / self.directory
self.assertTrue((staged / "packages.adb").is_file())

def test_build_accepts_relative_apk_executable_when_subprocess_changes_directory(self) -> None:
relative_apk = Path(os.path.relpath(self.fake_apk, Path.cwd()))
self.assertFalse(relative_apk.is_absolute())
output, archive, _ = self.build(apk_executable=relative_apk)
self.assertTrue((output / "packages.adb").is_file())
self.assertTrue(archive.is_file())
self.assertEqual(len(self.fake_apk_calls()), 2)

def test_build_resolves_bare_apk_executable_from_path(self) -> None:
bin_dir = self.root / "bin"
bin_dir.mkdir()
path_apk = bin_dir / "apk"
path_apk.write_bytes(self.fake_apk.read_bytes())
path_apk.chmod(0o755)
caller = self.root / "caller"
caller.mkdir()
(caller / "apk").write_text("not executable\n", encoding="ascii")
output, archive, _ = self.build(
apk_executable=Path("apk"),
extra_env={"PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}"},
cwd=caller,
)
self.assertTrue((output / "packages.adb").is_file())
self.assertTrue(archive.is_file())
self.assertEqual(len(self.fake_apk_calls()), 2)

def test_build_rejects_non_executable_apk_path_before_staging(self) -> None:
non_executable = self.root / "not-executable-apk"
non_executable.write_text("#!/bin/sh\nexit 0\n", encoding="ascii")
output, archive, result = self.build(apk_executable=non_executable, expected=1)
self.assertIn("apk executable is not executable", result.stderr.lower())
self.assertFalse(output.exists())
self.assertFalse(archive.exists())

def test_build_rejects_wrong_locked_public_key(self) -> None:
wrong_private = self.root / "wrong-private.pem"
wrong_public = self.root / "wrong-public.pem"
Expand Down
Loading