Add a third builder shape for archives with no wrapping directory
Merging master's claude-code.toml onto this branch surfaced a real gap:
builder.rs only knew "bare binary download" and "tarball extracting into
a same-named directory" (uv). claude-code's tarball extracts a bare
`claude` file with no wrapping directory, and that inner filename
doesn't match the package name either — makepkg's package() failed with
"cannot stat .../claude-linux-x64/claude-code" (confirmed by actually
running the build).
Add Package::archive_binary_path, an explicit override for the
in-archive path builder.rs installs from, used verbatim when present
instead of the stem/binary_name convention. Set binary_name = "claude"
too, matching the box's actual command name (/opt/claude-code/bin/claude)
rather than the claude-code package name. Also added a sanity_check
block (claude --version), matching uv's pattern, confirmed against the
real built binary's output ("2.1.276 (Claude Code)").
Verified end to end against the real repo with PKGWATCH_REPO_DIR
pointed at a scratch dir: check -> fetch -> verify -> review --approve
-> build -> sanity-check -> publish all pass for claude-code v2.1.276.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
14e1cee36f
commit
0044eda532
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
|
# 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
|
# glibc x86_64 Linux build (`claude-linux-x64.tar.gz`), not the musl
|
||||||
# variant, to match this machine.
|
# 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]
|
[package.claude-code]
|
||||||
repo = "anthropics/claude-code"
|
repo = "anthropics/claude-code"
|
||||||
asset_pattern = "claude-linux-x64.tar.gz"
|
asset_pattern = "claude-linux-x64.tar.gz"
|
||||||
|
binary_name = "claude"
|
||||||
|
archive_binary_path = "claude"
|
||||||
|
|
||||||
[package.claude-code.verification]
|
[package.claude-code.verification]
|
||||||
method = "same-origin-sha256"
|
method = "same-origin-sha256"
|
||||||
checksum_asset_pattern = "SHASUMS256.txt"
|
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`.
|
/// artifact, then runs `makepkg` in `build_dir`.
|
||||||
///
|
///
|
||||||
/// Deliberately one fixed "prebuilt binary" shape, not a templating engine
|
/// 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
|
/// currently-tracked packages actually need: a bare-binary download
|
||||||
/// (scaleway-cli) and a tarball containing a same-named directory (uv).
|
/// (scaleway-cli), a tarball containing a same-named directory (uv), and a
|
||||||
/// Extend when a third real shape shows up rather than guessing at
|
/// 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.
|
/// generality now.
|
||||||
pub fn build(req: &BuildRequest, build_dir: &Path) -> Result<BuildResult> {
|
pub fn build(req: &BuildRequest, build_dir: &Path) -> Result<BuildResult> {
|
||||||
let pkgbuild = generate_pkgbuild(req)?;
|
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 sha256 = hash::sha256_hex_file(req.artifact_path)?;
|
||||||
|
|
||||||
let install_source = match archive_stem(req.asset_name) {
|
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}"),
|
Some(stem) => format!("{stem}/{binary_name}"),
|
||||||
None => req.asset_name.to_string(),
|
None => req.asset_name.to_string(),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
|
|
@ -328,6 +335,23 @@ mod tests {
|
||||||
toml::from_str(&toml_text).unwrap()
|
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]
|
#[test]
|
||||||
fn build_rejects_unsafe_version() {
|
fn build_rejects_unsafe_version() {
|
||||||
let pkg = make_package(None);
|
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]
|
#[test]
|
||||||
fn generate_pkgbuild_rejects_download_url_with_single_quote() {
|
fn generate_pkgbuild_rejects_download_url_with_single_quote() {
|
||||||
let pkg = make_package(None);
|
let pkg = make_package(None);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,16 @@ pub struct Package {
|
||||||
/// checking the currently-installed extra package, not guessable from
|
/// checking the currently-installed extra package, not guessable from
|
||||||
/// the repo name). Defaults to the package name when omitted.
|
/// the repo name). Defaults to the package name when omitted.
|
||||||
pub binary_name: Option<String>,
|
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
|
/// Post-build correctness check (not a security control — see
|
||||||
/// docs/SPEC.md > Verification trust tiers). Runs `command` against the
|
/// docs/SPEC.md > Verification trust tiers). Runs `command` against the
|
||||||
/// freshly built binary and confirms `version_regex`'s capture group
|
/// freshly built binary and confirms `version_regex`'s capture group
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue