Close the loop: build, sanity-check, and publish #1
3 changed files with 101 additions and 6 deletions
|
|
@ -16,11 +16,25 @@
|
|||
# no `{version}` placeholder is needed, same as uv's config. Tracking the
|
||||
# glibc x86_64 Linux build (`claude-linux-x64.tar.gz`), not the musl
|
||||
# variant, to match this machine.
|
||||
#
|
||||
# Archive shape doesn't match uv's or scaleway-cli's: the tarball extracts a
|
||||
# bare `claude` file with no wrapping directory (confirmed via `tar tzvf`
|
||||
# against the real v2.1.276 asset) — hence archive_binary_path below (see
|
||||
# builder.rs's third shape). binary_name is also set explicitly to `claude`
|
||||
# (the real upstream command name, not the `claude-code` package name) so
|
||||
# the installed binary matches what this box already invokes as `claude`
|
||||
# (see /opt/claude-code/bin/claude).
|
||||
|
||||
[package.claude-code]
|
||||
repo = "anthropics/claude-code"
|
||||
asset_pattern = "claude-linux-x64.tar.gz"
|
||||
binary_name = "claude"
|
||||
archive_binary_path = "claude"
|
||||
|
||||
[package.claude-code.verification]
|
||||
method = "same-origin-sha256"
|
||||
checksum_asset_pattern = "SHASUMS256.txt"
|
||||
|
||||
[package.claude-code.sanity_check]
|
||||
command = "claude --version"
|
||||
version_regex = '(\d+\.\d+\.\d+) \(Claude Code\)'
|
||||
|
|
|
|||
|
|
@ -36,10 +36,12 @@ pub struct BuildResult {
|
|||
/// artifact, then runs `makepkg` in `build_dir`.
|
||||
///
|
||||
/// Deliberately one fixed "prebuilt binary" shape, not a templating engine
|
||||
/// — see docs/SPEC.md > Scaling > Template reuse. Covers the two shapes the two
|
||||
/// — see docs/SPEC.md > Scaling > Template reuse. Covers the shapes
|
||||
/// currently-tracked packages actually need: a bare-binary download
|
||||
/// (scaleway-cli) and a tarball containing a same-named directory (uv).
|
||||
/// Extend when a third real shape shows up rather than guessing at
|
||||
/// (scaleway-cli), a tarball containing a same-named directory (uv), and a
|
||||
/// tarball with no wrapping directory at all whose inner filename doesn't
|
||||
/// match the package name (claude-code — see `Package::archive_binary_path`).
|
||||
/// Extend when a fourth real shape shows up rather than guessing at
|
||||
/// generality now.
|
||||
pub fn build(req: &BuildRequest, build_dir: &Path) -> Result<BuildResult> {
|
||||
let pkgbuild = generate_pkgbuild(req)?;
|
||||
|
|
@ -86,9 +88,14 @@ fn generate_pkgbuild(req: &BuildRequest) -> Result<String> {
|
|||
|
||||
let sha256 = hash::sha256_hex_file(req.artifact_path)?;
|
||||
|
||||
let install_source = match archive_stem(req.asset_name) {
|
||||
Some(stem) => format!("{stem}/{binary_name}"),
|
||||
None => req.asset_name.to_string(),
|
||||
let install_source = if let Some(path) = &req.pkg.archive_binary_path {
|
||||
validate_shell_safe("archive binary path", path)?;
|
||||
path.clone()
|
||||
} else {
|
||||
match archive_stem(req.asset_name) {
|
||||
Some(stem) => format!("{stem}/{binary_name}"),
|
||||
None => req.asset_name.to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(format!(
|
||||
|
|
@ -328,6 +335,23 @@ mod tests {
|
|||
toml::from_str(&toml_text).unwrap()
|
||||
}
|
||||
|
||||
fn make_package_with_archive_binary_path(
|
||||
binary_name: &str,
|
||||
archive_binary_path: &str,
|
||||
) -> Package {
|
||||
let toml_text = format!(
|
||||
r#"
|
||||
repo = "o/r"
|
||||
asset_pattern = "x"
|
||||
binary_name = "{binary_name}"
|
||||
archive_binary_path = "{archive_binary_path}"
|
||||
[verification]
|
||||
method = "github-attestation"
|
||||
"#
|
||||
);
|
||||
toml::from_str(&toml_text).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_rejects_unsafe_version() {
|
||||
let pkg = make_package(None);
|
||||
|
|
@ -402,6 +426,53 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_pkgbuild_flat_archive_installs_from_archive_binary_path_override() {
|
||||
// claude-code's shape: a tarball with no wrapping directory, whose
|
||||
// inner filename ("claude") doesn't match the package name
|
||||
// ("claude-code") — neither existing shape (stem/binary_name, or
|
||||
// bare-binary-no-archive) fits, hence the explicit override.
|
||||
let pkg = make_package_with_archive_binary_path("claude-code", "claude");
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let artifact_path = dir.path().join("claude-linux-x64.tar.gz");
|
||||
std::fs::write(&artifact_path, b"tarball-bytes").unwrap();
|
||||
|
||||
let req = BuildRequest {
|
||||
pkg_name: "claude-code",
|
||||
pkg: &pkg,
|
||||
version: "2.1.276",
|
||||
repo: "anthropics/claude-code",
|
||||
asset_name: "claude-linux-x64.tar.gz",
|
||||
download_url: "https://github.com/anthropics/claude-code/releases/download/v2.1.276/claude-linux-x64.tar.gz",
|
||||
artifact_path: &artifact_path,
|
||||
};
|
||||
let pkgbuild = generate_pkgbuild(&req).unwrap();
|
||||
|
||||
assert!(
|
||||
pkgbuild
|
||||
.contains("install -Dm755 \"${srcdir}/claude\" \"${pkgdir}/usr/bin/claude-code\"")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_pkgbuild_rejects_archive_binary_path_with_command_substitution() {
|
||||
let pkg = make_package_with_archive_binary_path("claude-code", "claude$(touch pwned)");
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let artifact_path = dir.path().join("claude-linux-x64.tar.gz");
|
||||
std::fs::write(&artifact_path, b"data").unwrap();
|
||||
|
||||
let req = BuildRequest {
|
||||
pkg_name: "claude-code",
|
||||
pkg: &pkg,
|
||||
version: "2.1.276",
|
||||
repo: "anthropics/claude-code",
|
||||
asset_name: "claude-linux-x64.tar.gz",
|
||||
download_url: "https://github.com/anthropics/claude-code/releases/download/v2.1.276/claude-linux-x64.tar.gz",
|
||||
artifact_path: &artifact_path,
|
||||
};
|
||||
assert!(generate_pkgbuild(&req).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_pkgbuild_rejects_download_url_with_single_quote() {
|
||||
let pkg = make_package(None);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@ pub struct Package {
|
|||
/// checking the currently-installed extra package, not guessable from
|
||||
/// the repo name). Defaults to the package name when omitted.
|
||||
pub binary_name: Option<String>,
|
||||
/// Explicit path to the binary inside the extracted archive, relative
|
||||
/// to `srcdir`, for archive layouts that don't match the "extracts into
|
||||
/// a directory named after the archive stem" convention `builder.rs`
|
||||
/// otherwise assumes (e.g. claude-code's tarball extracts a bare
|
||||
/// `claude` file with no wrapping directory, and that inner filename
|
||||
/// doesn't match the package name either). Only meaningful when
|
||||
/// `asset_pattern` names an archive; ignored for bare-binary downloads,
|
||||
/// where the downloaded file *is* the source path already. Defaults to
|
||||
/// the stem/`binary_name` convention when omitted.
|
||||
pub archive_binary_path: Option<String>,
|
||||
/// Post-build correctness check (not a security control — see
|
||||
/// docs/SPEC.md > Verification trust tiers). Runs `command` against the
|
||||
/// freshly built binary and confirms `version_regex`'s capture group
|
||||
|
|
|
|||
Loading…
Reference in a new issue