feat: unify source and skills for Dever 0.1.2 installation
This commit is contained in:
1 parent
a2e0436ed4
commit
19f122b4b4
47 files changed
+1951
-63
No files matched your search
@@ -116,6 +116,193 @@ fn missing_pack_fails_without_host_tools_and_check_and_empty_test_need_no_pack()
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_check_rejects_redundancy_before_preparing_build_resources() {
|
||||
let toolchain = TemporaryDirectory::new();
|
||||
let compiler = compiler(toolchain.path(), "dever");
|
||||
let project_dir = TemporaryDirectory::new();
|
||||
let root = project_dir.path();
|
||||
write(root, "config/setting.json", "{}");
|
||||
write(
|
||||
root,
|
||||
"module/sample/profile/domain.dever",
|
||||
"normalize(value: Text) (result: Text) pure { result = text.trim(value) }\nalias(value: Text) (result: Text) pure { result = normalize(value) }\norphan() (result: Text) pure { result = \"unused\" }",
|
||||
);
|
||||
write(
|
||||
root,
|
||||
"module/sample/profile/app.dever",
|
||||
"greet(value: Text) (result: Text) { result = domain.alias(value) }\nunused() (result: Text) { result = \"unused\" }",
|
||||
);
|
||||
write(
|
||||
root,
|
||||
"module/sample/profile/api.dever",
|
||||
"cmd greet = app.greet",
|
||||
);
|
||||
let checked = cli(&compiler, root, "check", &[]);
|
||||
failure(&checked, "C016");
|
||||
let diagnostic = String::from_utf8_lossy(&checked.stderr);
|
||||
for expected in [
|
||||
"sample/profile/app.dever:2:1: error[C016]",
|
||||
"function 'sample.profile.unused' is unreachable",
|
||||
"sample/profile/domain.dever:2:1: error[C016]",
|
||||
"only forwards unchanged inputs",
|
||||
"sample/profile/domain.dever:3:1: error[C016]",
|
||||
"function 'sample.profile.domain.orphan' is unreachable",
|
||||
] {
|
||||
assert!(diagnostic.contains(expected), "{expected}: {diagnostic}");
|
||||
}
|
||||
// The same source fails before native packs, output publication or execution.
|
||||
let executable = root.join("must-not-build");
|
||||
for (action, args) in [
|
||||
("run", Vec::new()),
|
||||
("build", vec!["--output", executable.to_str().unwrap()]),
|
||||
] {
|
||||
let rejected = cli(&compiler, root, action, &args);
|
||||
assert!(!rejected.status.success());
|
||||
assert_eq!(rejected.stderr, checked.stderr);
|
||||
}
|
||||
assert!(!executable.exists());
|
||||
assert!(!root.join("data").exists());
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_check_rejects_plain_model_get_wrapper_without_opening_database() {
|
||||
let toolchain = TemporaryDirectory::new();
|
||||
let compiler = compiler(toolchain.path(), "dever");
|
||||
let project_dir = TemporaryDirectory::new();
|
||||
let root = project_dir.path();
|
||||
write(root, "config/setting.json", "{}");
|
||||
write(
|
||||
root,
|
||||
"module/sample/profile/model.dever",
|
||||
"type Profile { name: Text }",
|
||||
);
|
||||
write(
|
||||
root,
|
||||
"module/sample/profile/app.dever",
|
||||
"lookup(id: model.id) (profile: model.Profile) { profile = model.get(id) }",
|
||||
);
|
||||
write(
|
||||
root,
|
||||
"module/sample/profile/api.dever",
|
||||
"cmd lookup = app.lookup",
|
||||
);
|
||||
let rejected = cli(&compiler, root, "check", &[]);
|
||||
failure(&rejected, "C016");
|
||||
let diagnostic = String::from_utf8_lossy(&rejected.stderr);
|
||||
for expected in [
|
||||
"sample/profile/app.dever:1:1: error[C016]",
|
||||
"only forwards a Model CRUD operation; declare rest for plain CRUD",
|
||||
] {
|
||||
assert!(diagnostic.contains(expected), "{expected}: {diagnostic}");
|
||||
}
|
||||
assert!(!root.join("data").exists());
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_check_accepts_both_templates_and_preserves_markdown_locations() {
|
||||
let toolchain = TemporaryDirectory::new();
|
||||
let compiler = compiler(toolchain.path(), "dever");
|
||||
let workspace = TemporaryDirectory::new();
|
||||
for markdown in [false, true] {
|
||||
let root = workspace
|
||||
.path()
|
||||
.join(if markdown { "markdown" } else { "plain" });
|
||||
dever_cli::project::create(&root, markdown).unwrap();
|
||||
success(&cli(&compiler, &root, "check", &[]));
|
||||
// Removing the production declaration must not make the test a production root.
|
||||
let suffix = if markdown { "dever.md" } else { "dever" };
|
||||
fs::remove_file(root.join(format!("module/hello/greeting/api.{suffix}"))).unwrap();
|
||||
let rejected = cli(&compiler, &root, "check", &[]);
|
||||
failure(&rejected, "C016");
|
||||
failure(
|
||||
&rejected,
|
||||
"unreachable from API, CMD, or Job production entries",
|
||||
);
|
||||
let app = root.join(format!("module/hello/greeting/app.{suffix}"));
|
||||
let source = fs::read_to_string(&app).unwrap();
|
||||
let line = source
|
||||
.lines()
|
||||
.position(|line| line.starts_with("greet("))
|
||||
.unwrap()
|
||||
+ 1;
|
||||
failure(&rejected, &format!("hello/greeting/app.{suffix}:{line}:1"));
|
||||
assert!(!root.join("data").exists());
|
||||
}
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_check_needs_no_database_worker_or_runtime_pack() {
|
||||
let toolchain = TemporaryDirectory::new();
|
||||
let compiler = compiler(toolchain.path(), "dever");
|
||||
let project_dir = TemporaryDirectory::new();
|
||||
let root = project_dir.path();
|
||||
write(root, "config/setting.json", "{}");
|
||||
write(
|
||||
root,
|
||||
"module/sample/profile/model.dever",
|
||||
"type Profile { name: Text }",
|
||||
);
|
||||
// A model-only project has no App/Domain code to reject, even without entries.
|
||||
success(&cli(&compiler, root, "check", &[]));
|
||||
write(
|
||||
root,
|
||||
"module/notification/mail/app.dever",
|
||||
"read() (value: Int) { value = port.read() }",
|
||||
);
|
||||
write(
|
||||
root,
|
||||
"module/notification/mail/port.dever",
|
||||
"read() (value: Int) fails dever.time.SleepResult",
|
||||
);
|
||||
write(
|
||||
root,
|
||||
"module/notification/mail/adapter.dever",
|
||||
"external exec \"worker/must-not-start\" {}",
|
||||
);
|
||||
write(
|
||||
root,
|
||||
"module/notification/mail/api.dever",
|
||||
"cmd read = app.read",
|
||||
);
|
||||
success(&cli(&compiler, root, "check", &[]));
|
||||
assert!(!root.join("data").exists());
|
||||
assert!(!root.join("dever.lock").exists());
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn application_check_does_not_apply_production_reachability_to_tests() {
|
||||
let toolchain = TemporaryDirectory::new();
|
||||
let compiler = compiler(toolchain.path(), "dever");
|
||||
let project_dir = TemporaryDirectory::new();
|
||||
let root = project_dir.path();
|
||||
write(
|
||||
root,
|
||||
"module/sample/profile/app.dever",
|
||||
"greet() (value: Text) { value = \"hello\" }",
|
||||
);
|
||||
write(root, "config/setting.json", "{}");
|
||||
failure(&cli(&compiler, root, "check", &[]), "C016");
|
||||
let empty = cli(&compiler, root, "test", &[]);
|
||||
success(&empty);
|
||||
assert!(String::from_utf8_lossy(&empty.stdout).contains("0 passed; 0 failed"));
|
||||
write(
|
||||
root,
|
||||
"test/sample/profile/greeting.dever",
|
||||
"greeting() () { assert_eq(app.greet(), \"hello\") }",
|
||||
);
|
||||
// The real case reaches the test compiler's missing-pack boundary, not C016.
|
||||
let tests = cli(&compiler, root, "test", &[]);
|
||||
failure(&tests, "runtime pack");
|
||||
assert!(!String::from_utf8_lossy(&tests.stderr).contains("C016"));
|
||||
assert!(!String::from_utf8_lossy(&tests.stdout).contains("C016"));
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_build_requires_the_selected_pack_even_when_host_pack_exists() {
|
||||
let root = TemporaryDirectory::new();
|
||||
|
||||
@@ -108,22 +108,24 @@ fn signed_catalog_paths_and_total_size_are_bounded_before_extraction() {
|
||||
|
||||
#[test]
|
||||
fn official_redirects_reject_http_credentials_ports_and_foreign_hosts() {
|
||||
let current = "https://github.com/shemic/dever-main/releases/latest/download/a";
|
||||
let current = "https://git.dever.cc/shemic/dever/dist/latest/a";
|
||||
assert!(
|
||||
redirect(
|
||||
current,
|
||||
"https://release-assets.githubusercontent.com/asset?token=opaque"
|
||||
"https://git.dever.cc/shemic/dever/dist/1.2.3/a?token=opaque"
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(redirect(current, "/shemic/dever-main/releases/download/v1.2.3/a").is_ok());
|
||||
assert!(redirect(current, "/shemic/dever/dist/1.2.3/a").is_ok());
|
||||
for address in [
|
||||
"http://github.com/a",
|
||||
"http://git.dever.cc/a",
|
||||
"https://evil.example/a",
|
||||
"https://github.com.evil.example/a",
|
||||
"https://user@github.com/a",
|
||||
"https://github.com:444/a",
|
||||
"https://github.com/a#fragment",
|
||||
"https://git.dever.cc.evil.example/a",
|
||||
"https://user@git.dever.cc/a",
|
||||
"https://git.dever.cc:444/a",
|
||||
"https://git.dever.cc/a#fragment",
|
||||
"https://github.com/a",
|
||||
"https://release-assets.githubusercontent.com/a",
|
||||
] {
|
||||
assert!(redirect(current, address).is_err(), "{address}");
|
||||
}
|
||||
@@ -135,14 +137,21 @@ fn official_asset_names_are_versioned_and_platform_specific() {
|
||||
assert_eq!(
|
||||
source.asset("latest", "manifest.json"),
|
||||
format!(
|
||||
"{OFFICIAL_RELEASES}/latest/download/dever-{}.manifest.json",
|
||||
"https://git.dever.cc/shemic/dever/dist/latest/dever-{}.manifest.json",
|
||||
platform_identity()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
source.asset("1.2.3", "tar.zst"),
|
||||
format!(
|
||||
"{OFFICIAL_RELEASES}/download/v1.2.3/dever-{}.tar.zst",
|
||||
"https://git.dever.cc/shemic/dever/dist/1.2.3/dever-{}.tar.zst",
|
||||
platform_identity()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
source.asset("1.2.3", "ext-runtime-pip-linux-x86_64.tar.zst"),
|
||||
format!(
|
||||
"https://git.dever.cc/shemic/dever/dist/1.2.3/dever-{}.ext-runtime-pip-linux-x86_64.tar.zst",
|
||||
platform_identity()
|
||||
)
|
||||
);
|
||||
@@ -174,7 +183,7 @@ fn server(responses: Vec<Vec<u8>>) -> (ReleaseSource, std::thread::JoinHandle<()
|
||||
let base = format!("http://{}/releases", listener.local_addr().unwrap());
|
||||
let worker = std::thread::spawn(move || {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
for response in responses {
|
||||
for (index, response) in responses.into_iter().enumerate() {
|
||||
let mut stream = loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, _)) => break stream,
|
||||
@@ -194,7 +203,18 @@ fn server(responses: Vec<Vec<u8>>) -> (ReleaseSource, std::thread::JoinHandle<()
|
||||
let mut reader = io::BufReader::new(stream.try_clone().unwrap());
|
||||
let mut request = String::new();
|
||||
reader.read_line(&mut request).unwrap();
|
||||
assert!(request.starts_with("GET /releases/"));
|
||||
let (selection, suffix) = [
|
||||
("latest", "manifest.json"),
|
||||
("latest", "manifest.sig"),
|
||||
("1.2.3", "tar.zst"),
|
||||
][index];
|
||||
assert_eq!(
|
||||
request,
|
||||
format!(
|
||||
"GET /releases/{selection}/dever-{}.{suffix} HTTP/1.1\r\n",
|
||||
platform_identity()
|
||||
)
|
||||
);
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).unwrap();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Standalone Bash contract; subprocesses never install or change services."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
spec = importlib.util.spec_from_file_location("dever_install_entry", ROOT / "sdk/install-entry.py")
|
||||
generator = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(generator)
|
||||
|
||||
|
||||
class InstallEntryTests(unittest.TestCase):
|
||||
def run_bash(self, script, *arguments, **kwargs):
|
||||
return subprocess.run(["/bin/bash", "-s", "--", *arguments], input=script,
|
||||
env={}, text=True, capture_output=True, timeout=10, **kwargs)
|
||||
|
||||
def test_generated_entry_matches_source_and_independent_key(self):
|
||||
expected = generator.render((ROOT / "skills/scripts/install.py").read_text(),
|
||||
(ROOT / "skills/assets/release.pub").read_text())
|
||||
self.assertEqual((ROOT / "install").read_text(), expected)
|
||||
syntax = subprocess.run(["/bin/bash", "-n", str(ROOT / "install")], env={},
|
||||
text=True, capture_output=True, timeout=10)
|
||||
self.assertEqual(syntax.returncode, 0, syntax.stderr)
|
||||
|
||||
def test_file_and_pipe_help_do_not_need_a_checkout(self):
|
||||
with tempfile.TemporaryDirectory(prefix="dever-entry-") as directory:
|
||||
entry = Path(directory) / "install"
|
||||
entry.write_bytes((ROOT / "install").read_bytes())
|
||||
for command, contents in ((["/bin/bash", str(entry), "--help"], None),
|
||||
(["/bin/bash", "-s", "--", "--help"], entry.read_text())):
|
||||
result = subprocess.run(command, input=contents, cwd=directory, env={},
|
||||
text=True, capture_output=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("--trusted-key", result.stdout)
|
||||
self.assertIn("--system-root", result.stdout)
|
||||
invalid = self.run_bash((ROOT / "install").read_text(), "--unknown-argument")
|
||||
self.assertEqual(invalid.returncode, 2)
|
||||
|
||||
def test_arguments_python_isolation_and_exit_code_are_preserved(self):
|
||||
source = '''import json
|
||||
import sys
|
||||
def main(default_key):
|
||||
print(json.dumps([sys.argv[1:], default_key, sys.flags.isolated]))
|
||||
raise SystemExit(37)
|
||||
'''
|
||||
key = "ab" * 32
|
||||
script = generator.render(source, key)
|
||||
arguments = ["--release", "/a path/with 'quotes'", "", "$(not executed)", "*", "--no-start"]
|
||||
result = self.run_bash(script, *arguments)
|
||||
self.assertEqual(result.returncode, 37, result.stderr)
|
||||
self.assertEqual(json.loads(result.stdout), [arguments, key, 1])
|
||||
|
||||
def test_truncated_body_does_not_execute_python_or_sudo(self):
|
||||
script = generator.render('def main(default_key):\n print("EXECUTED")\n', "ab" * 32)
|
||||
for end in (script.index("import sys"), script.index("DEVER_INSTALL_PYTHON\n}"),
|
||||
script.index('\ndever_install "$@"')):
|
||||
result = self.run_bash(script[:end])
|
||||
self.assertNotIn("EXECUTED", result.stdout)
|
||||
|
||||
@unittest.skipUnless(os.geteuid() == 0 and Path("/usr/bin/sudo").is_file(),
|
||||
"real nonroot UID fixture requires root and sudo executable")
|
||||
def test_nonroot_help_skips_sudo_and_install_forwards_through_sudo(self):
|
||||
# Replace only the sudo command in this owned Bash process. It records
|
||||
# argv and returns without executing Python or attempting escalation.
|
||||
intercept = '''function /usr/bin/sudo() {
|
||||
printf '<%s>\\n' "$@"
|
||||
return 43
|
||||
}
|
||||
'''
|
||||
script = intercept + (ROOT / "install").read_text()
|
||||
lowered = {"user": 65534, "group": 65534, "extra_groups": []}
|
||||
help_result = self.run_bash(script, "--help", **lowered)
|
||||
self.assertEqual(help_result.returncode, 0, help_result.stderr)
|
||||
self.assertIn("--release", help_result.stdout)
|
||||
install = self.run_bash(script, "--system-root", "/owned image", "--no-start", **lowered)
|
||||
self.assertEqual(install.returncode, 43, install.stderr)
|
||||
self.assertEqual(install.stdout.splitlines(), [
|
||||
"<-->", "</usr/bin/python3>", "<-I>", "<->", "<--system-root>",
|
||||
"</owned image>", "<--no-start>",
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -212,6 +212,8 @@ class InstallerTests(unittest.TestCase):
|
||||
calls = []
|
||||
|
||||
def download(address):
|
||||
self.assertTrue(address.startswith(
|
||||
f"https://git.dever.cc/shemic/dever/dist/1.2.3/dever-{self.platform}."))
|
||||
return (assets_path / address.rsplit("/", 1)[-1]).open("rb")
|
||||
|
||||
def extract(command, **kwargs):
|
||||
@@ -298,6 +300,88 @@ class InstallerTests(unittest.TestCase):
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(list(image.iterdir()), [])
|
||||
|
||||
def test_metadata_uses_latest_or_exact_directory_and_never_a_github_fallback(self):
|
||||
for version in ("latest", "1.2.3"):
|
||||
destination = self.directory(version)
|
||||
addresses = []
|
||||
|
||||
def download(address):
|
||||
addresses.append(address)
|
||||
return io.BytesIO(b"metadata")
|
||||
|
||||
with mock.patch.object(installer, "download", side_effect=download):
|
||||
installer.metadata(destination, None, version, self.platform)
|
||||
self.assertEqual(addresses, [
|
||||
f"https://git.dever.cc/shemic/dever/dist/{version}/dever-{self.platform}.{suffix}"
|
||||
for suffix in ("manifest.json", "manifest.sig")
|
||||
])
|
||||
|
||||
def test_redirects_remain_on_the_official_https_site(self):
|
||||
handler = installer.OfficialRedirect()
|
||||
current = "https://git.dever.cc/shemic/dever/dist/latest/a"
|
||||
request = installer.urllib.request.Request(current)
|
||||
allowed = "https://git.dever.cc/shemic/dever/dist/1.2.3/a"
|
||||
redirected = handler.redirect_request(request, None, 302, "Found", {}, allowed)
|
||||
self.assertEqual(redirected.full_url, allowed)
|
||||
for address in (
|
||||
"http://git.dever.cc/a", "https://evil.example/a",
|
||||
"https://git.dever.cc.evil.example/a", "https://user@git.dever.cc/a",
|
||||
"https://git.dever.cc:444/a", "https://git.dever.cc/a#fragment",
|
||||
"https://github.com/a", "https://release-assets.githubusercontent.com/a"):
|
||||
with self.subTest(address=address), self.assertRaises(ValueError):
|
||||
handler.redirect_request(request, None, 302, "Found", {}, address)
|
||||
|
||||
def test_prerequisites_reject_unsupported_hosts_before_download(self):
|
||||
with mock.patch.object(installer.os, "confstr", return_value="glibc 2.39"):
|
||||
installer.prerequisites("linux-x86_64")
|
||||
for libc in ("glibc 2.38", "musl 1.2", ""):
|
||||
with mock.patch.object(installer.os, "confstr", return_value=libc), \
|
||||
self.assertRaisesRegex(ValueError, "glibc"):
|
||||
installer.prerequisites("linux-x86_64")
|
||||
with mock.patch.object(installer.os, "access", return_value=False), \
|
||||
self.assertRaisesRegex(ValueError, "openssl"):
|
||||
installer.prerequisites("linux-x86_64")
|
||||
|
||||
@unittest.skipUnless(installer.os.geteuid() == 0, "owned image installation requires root")
|
||||
def test_standalone_bash_uses_embedded_key_and_cleans_after_signature_failure(self):
|
||||
# Execute an independently signed fixture to observe the entire entry
|
||||
# and bootstrap call chain, without installing a machine or service.
|
||||
generator = load_module("dever_install_entry", ROOT / "sdk/install-entry.py")
|
||||
launcher = self.release / "bootstrap/dever"
|
||||
launcher.write_text('''#!/usr/bin/python3
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
assert sys.argv[1] == "--dever-install"
|
||||
setting = json.loads((Path(sys.argv[2]) / "config/setting.json").read_text())
|
||||
assert setting["activate_service"] is False
|
||||
(Path(setting["system_root"]) / "called.json").write_text(json.dumps(setting))
|
||||
''')
|
||||
content = launcher.read_bytes()
|
||||
manifest = json.loads((self.release / "manifest.json").read_text())
|
||||
for artifact in manifest["artifacts"]:
|
||||
if artifact["path"] == "bootstrap/dever":
|
||||
artifact.update(bytes=len(content), sha256=hashlib.sha256(content).hexdigest())
|
||||
(self.release / "manifest.json").write_text(json.dumps(manifest))
|
||||
self.sign_manifest()
|
||||
entry = generator.render((ROOT / "skills/scripts/install.py").read_text(), self.public.read_text())
|
||||
image = self.directory("image")
|
||||
arguments = ["/bin/bash", "-s", "--", "--release", str(self.release),
|
||||
"--system-root", str(image), "--version", "1.2.3", "--no-start"]
|
||||
completed = subprocess.run(arguments, input=entry, env={}, text=True,
|
||||
capture_output=True, timeout=20)
|
||||
self.assertEqual(completed.returncode, 0, completed.stderr)
|
||||
self.assertTrue((image / "called.json").is_file())
|
||||
self.assertEqual(list(image.glob(".dever-install-*")), [])
|
||||
(image / "called.json").unlink()
|
||||
with (self.release / "manifest.json").open("a") as stream:
|
||||
stream.write(" ")
|
||||
rejected = subprocess.run(arguments, input=entry, env={}, text=True,
|
||||
capture_output=True, timeout=20)
|
||||
self.assertNotEqual(rejected.returncode, 0)
|
||||
self.assertIn("签名校验失败", rejected.stderr)
|
||||
self.assertEqual(list(image.iterdir()), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/python3
|
||||
"""Opt-in real Bash installer acceptance in an owned image, without host services."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from verify_prepared_release import WORKSPACE, execute
|
||||
|
||||
|
||||
def verify(args):
|
||||
result = {
|
||||
"status": "running", "checks": [],
|
||||
"transport": "local signed assets; actual generated Bash entry and Rust bootstrap",
|
||||
"online_install_and_update": "not_run",
|
||||
"entry_sha256": hashlib.sha256(args.entry.read_bytes()).hexdigest(),
|
||||
}
|
||||
|
||||
def passed(name):
|
||||
result["checks"].append(name)
|
||||
args.report.write_text(json.dumps(result, indent=2) + "\n")
|
||||
print(f"PASS {name}", flush=True)
|
||||
|
||||
try:
|
||||
with tempfile.TemporaryDirectory(prefix="dever-install-entry-", dir=args.scratch_parent) as directory:
|
||||
scratch = Path(directory)
|
||||
release = scratch / "release"
|
||||
release.mkdir()
|
||||
for name in ("manifest.json", "manifest.sig"):
|
||||
shutil.copyfile(args.release / name, release / name)
|
||||
manifest = json.loads((release / "manifest.json").read_bytes())
|
||||
if manifest["platform"] != "linux-x86_64":
|
||||
raise ValueError("this acceptance fixture requires a Linux x86_64 release")
|
||||
result["payload_version"] = manifest["version"]
|
||||
result["manifest_sha256"] = hashlib.sha256((release / "manifest.json").read_bytes()).hexdigest()
|
||||
key = scratch / "release.pub"
|
||||
shutil.copyfile(WORKSPACE / "skills/assets/release.pub", key)
|
||||
extraction = scratch / "extract.json"
|
||||
extraction.write_text(json.dumps({
|
||||
"release": str(release),
|
||||
"archive": str(args.assets / "dever-linux-x86_64.tar.zst"),
|
||||
"trusted_key": str(key),
|
||||
}))
|
||||
execute([args.extractor, "--dever-extract", extraction])
|
||||
passed("prepared_release_authenticated_by_existing_rust_extractor")
|
||||
|
||||
# A copied standalone entry has no adjacent skill or source checkout.
|
||||
entry = scratch / "install"
|
||||
shutil.copyfile(args.entry, entry)
|
||||
image = scratch / "image"
|
||||
image.mkdir()
|
||||
arguments = ["--release", str(release), "--system-root", str(image), "--no-start"]
|
||||
execute(["/bin/bash", entry, *arguments])
|
||||
machine = image / "opt/dever"
|
||||
launcher = machine / "bin/dever"
|
||||
installed = machine / "versions" / manifest["version"]
|
||||
if execute([launcher, "version"]) != manifest["version"]:
|
||||
raise RuntimeError("installed launcher selected the wrong version")
|
||||
skill = installed / "skills/dever-language"
|
||||
if execute([launcher, "skill", "path"]) != str(skill):
|
||||
raise RuntimeError("installed skill does not match the selected core")
|
||||
if (image / "usr/local/bin/dever").readlink() != Path("/opt/dever/bin/dever"):
|
||||
raise RuntimeError("public launcher symlink was not installed")
|
||||
passed("standalone_bash_first_install_and_matching_skill")
|
||||
|
||||
tracked = [launcher, machine / "bin/deverd",
|
||||
installed / "manifest.json", installed / "dever-core"]
|
||||
baseline = {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in tracked}
|
||||
|
||||
def unchanged():
|
||||
current = {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in tracked}
|
||||
if current != baseline:
|
||||
raise RuntimeError("existing installed state changed")
|
||||
if list(image.glob(".dever-install-*")):
|
||||
raise RuntimeError("installer left private staging behind")
|
||||
|
||||
repeated = subprocess.run(
|
||||
["/bin/bash", "-s", "--", *arguments], input=entry.read_bytes(),
|
||||
cwd=scratch, env={}, capture_output=True, timeout=180,
|
||||
)
|
||||
if repeated.returncode:
|
||||
raise RuntimeError(repeated.stderr.decode(errors="replace"))
|
||||
unchanged()
|
||||
# Bootstrap records another activation even for the same version.
|
||||
# Compare the selected version here, then freeze the journal for failure checks.
|
||||
if execute([launcher, "version"]) != manifest["version"]:
|
||||
raise RuntimeError("reinstall changed the selected version")
|
||||
active = machine / "state/active-version"
|
||||
tracked.append(active)
|
||||
baseline[str(active)] = hashlib.sha256(active.read_bytes()).hexdigest()
|
||||
passed("pipe_reinstall_preserves_installed_state_and_cleans_staging")
|
||||
|
||||
(release / "manifest.sig").write_text("00" * 64 + "\n")
|
||||
rejected = subprocess.run(
|
||||
["/bin/bash", entry, *arguments], cwd=scratch, env={},
|
||||
capture_output=True, timeout=30,
|
||||
)
|
||||
if rejected.returncode == 0 or "签名校验失败" not in rejected.stderr.decode(errors="replace"):
|
||||
raise RuntimeError("corrupt signature did not fail at authentication")
|
||||
unchanged()
|
||||
if execute([launcher, "version"]) != manifest["version"]:
|
||||
raise RuntimeError("failed install changed the selected version")
|
||||
passed("corrupt_signature_rejected_existing_install_preserved")
|
||||
result["status"] = "passed"
|
||||
except BaseException as error:
|
||||
result["status"] = "failed"
|
||||
result["error"] = str(error)
|
||||
raise
|
||||
finally:
|
||||
args.report.write_text(json.dumps(result, indent=2) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
for name in ("release", "assets", "extractor", "scratch-parent", "report"):
|
||||
parser.add_argument(f"--{name}", required=True, type=Path)
|
||||
parser.add_argument("--entry", type=Path, default=WORKSPACE / "install")
|
||||
args = parser.parse_args()
|
||||
if os.geteuid() != 0:
|
||||
parser.error("requires root for its own image; never installs host services")
|
||||
for name, value in vars(args).items():
|
||||
setattr(args, name, value.absolute())
|
||||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
verify(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -202,14 +202,14 @@ def install_assets(assets, image, manifest):
|
||||
stem = f"dever-{manifest['platform']}"
|
||||
base = installer.RELEASES
|
||||
routes = {
|
||||
f"{base}/latest/download/{stem}.manifest.json": assets / f"{stem}.manifest.json",
|
||||
f"{base}/latest/download/{stem}.manifest.sig": assets / f"{stem}.manifest.sig",
|
||||
f"{base}/download/v{manifest['version']}/{stem}.tar.zst": assets / f"{stem}.tar.zst",
|
||||
f"{base}/latest/{stem}.manifest.json": assets / f"{stem}.manifest.json",
|
||||
f"{base}/latest/{stem}.manifest.sig": assets / f"{stem}.manifest.sig",
|
||||
f"{base}/{manifest['version']}/{stem}.tar.zst": assets / f"{stem}.tar.zst",
|
||||
}
|
||||
for artifact in manifest["artifacts"]:
|
||||
if artifact["path"] == "bootstrap/dever" or artifact["path"].startswith("bootstrap/lib/"):
|
||||
name = f"{stem}.blob-{artifact['sha256']}"
|
||||
routes[f"{base}/download/v{manifest['version']}/{name}"] = assets / name
|
||||
routes[f"{base}/{manifest['version']}/{name}"] = assets / name
|
||||
installer.download = lambda address: routes[address].open("rb")
|
||||
installer.install(argparse.Namespace(
|
||||
version="latest", release=None, system_root=image, no_start=True,
|
||||
@@ -225,7 +225,7 @@ def accept(release, assets, image, report, init, extension_helper=None):
|
||||
"version": manifest["version"], "platform": manifest["platform"],
|
||||
"manifest_sha256": hashlib.sha256((release / "manifest.json").read_bytes()).hexdigest(),
|
||||
"checks": [], "global_service_actions": "not_run", "online_update": "not_run",
|
||||
"installation_transport": "local_github_assets" if assets else "local_signed_directory",
|
||||
"installation_transport": "local_official_assets" if assets else "local_signed_directory",
|
||||
}
|
||||
|
||||
def passed(name):
|
||||
|
||||
Reference in new issue
Block a user