///|
/// Files of `dir` whose name satisfies `wanted`, as `PackageFile`s sorted by
/// name. Only the directory itself is looked at, not its subdirectories.
fn package_files_where(
dir : String,
wanted : (String) -> Bool,
) -> Array[@repo.PackageFile] raise CliError {
let names = @fs.read_dir(dir) catch {
err => raise Failed(describe_io(err, "cannot list \"\{dir}\""))
}
let found : Array[String] = []
for name in names {
if wanted(name) {
found.push(name)
}
}
found.sort()
found.map(name => { name, data: read_bytes(join_path(dir, name)), })
}
///|
/// Files of `dir` whose name ends with `suffix`.
fn package_files(
dir : String,
suffix : String,
) -> Array[@repo.PackageFile] raise CliError {
package_files_where(dir, name => name.has_suffix(suffix))
}
///|
fn is_arch_package(name : String) -> Bool {
name.contains(".pkg.tar") && !name.has_suffix(".sig")
}
///|
/// Remove metadata of an earlier run so that stale checksummed files do not
/// pile up in `repodata/`.
fn remove_old_repodata(dir : String) -> Unit raise CliError {
if !@fs.path_exists(dir) {
return
}
let names = @fs.read_dir(dir) catch {
err => raise Failed(describe_io(err, "cannot list \"\{dir}\""))
}
for name in names {
let ours = name == "repomd.xml" ||
name.has_suffix("-primary.xml.gz") ||
name.has_suffix("-filelists.xml.gz") ||
name.has_suffix("-other.xml.gz")
if ours {
@fs.remove_file(join_path(dir, name)) catch {
err => raise Failed(describe_io(err, "cannot remove \"\{name}\""))
}
}
}
}
///|
fn write_index(
dir : String,
files : Array[(String, Bytes)],
) -> Unit raise CliError {
for file in files {
let target = join_path(dir, file.0)
ensure_dir(dirname(target))
write_bytes(target, file.1)
println("wrote \{target} (\{file.1.length()} bytes)")
}
}
///|
/// The date of the metadata when `SOURCE_DATE_EPOCH` is not set: the newest
/// build time found in the packages, so running the command twice gives the
/// same files.
fn newest_time(
debs : Array[@repo.PackageFile],
rpms : Array[@repo.PackageFile],
archs : Array[@repo.PackageFile],
apks : Array[@repo.PackageFile],
) -> Int64 raise CliError {
let mut newest = 0L
let times : Array[Int64] = []
for deb in debs {
times.push(@repo.deb_time(deb) catch { err => raise Failed("\{err}") })
}
for rpm in rpms {
times.push(@repo.rpm_time(rpm) catch { err => raise Failed("\{err}") })
}
for pkg in archs {
times.push(@repo.arch_time(pkg) catch { err => raise Failed("\{err}") })
}
for pkg in apks {
times.push(@repo.apk_time(pkg) catch { err => raise Failed("\{err}") })
}
for t in times {
if t > newest {
newest = t
}
}
newest
}
///|
fn cmd_repo(args : Array[String]) -> Int32 raise CliError {
let options = parse_options(
args,
{
"--format": "format",
"-f": "format",
"--sign-key": "sign_key",
"--name": "name",
"-n": "name",
"--apk-key-name": "apk_key_name",
},
Map([]),
)
let dir = match options.positional {
[path] => path
[] => raise Usage("repo needs the directory that holds the packages")
_ => raise Usage("repo takes exactly one directory")
}
let format = match options.value("format") {
Some(f) => f
None => "all"
}
if format != "deb" &&
format != "rpm" &&
format != "arch" &&
format != "apk" &&
format != "all" {
raise Usage(
"unknown format \"\{format}\", expected deb, rpm, arch, apk or all",
)
}
if !@fs.path_exists(dir) {
raise Failed("\"\{dir}\" does not exist")
}
let signer = match options.value("sign_key") {
Some(path) => Some((load_signing_key(path), load_keyring(path)))
None => None
}
let wants = (kind : String) => format == kind || format == "all"
let debs = if wants("deb") { package_files(dir, ".deb") } else { [] }
let rpms = if wants("rpm") { package_files(dir, ".rpm") } else { [] }
let archs = if wants("arch") {
package_files_where(dir, is_arch_package)
} else {
[]
}
let apks = if wants("apk") { package_files(dir, ".apk") } else { [] }
if debs.length() == 0 &&
rpms.length() == 0 &&
archs.length() == 0 &&
apks.length() == 0 {
raise Failed("no \{format} packages found in \"\{dir}\"")
}
let stamp = match source_date_epoch() {
Some(t) => t
None => newest_time(debs, rpms, archs, apks)
}
if debs.length() > 0 {
let files = @repo.apt_index(debs, date=stamp) catch {
err => raise Failed("\{err}")
}
write_index(dir, files)
let problems = @repo.check_apt(files, debs)
if problems.length() > 0 {
raise Failed("the apt index is inconsistent: \{problems.join("; ")}")
}
match signer {
None =>
println(
"apt: \{debs.length()} package(s); use `deb [trusted=yes] file: ./`",
)
Some((key, ring)) => {
let signed = @repo.sign_apt(files, key, created=stamp) catch {
err => raise Failed("\{err}")
}
write_index(dir, signed)
let bad = @repo.check_apt_signature(files + signed, ring)
if bad.length() > 0 {
raise Failed("the apt signatures do not verify: \{bad.join("; ")}")
}
println(
"apt: \{debs.length()} package(s) signed by \{key.fingerprint()}; use `deb [signed-by=] file: ./`",
)
}
}
}
if rpms.length() > 0 {
let files = @repo.yum_index(rpms, timestamp=stamp) catch {
err => raise Failed("\{err}")
}
let repodata = join_path(dir, "repodata")
ensure_dir(repodata)
remove_old_repodata(repodata)
write_index(dir, files)
let problems = @repo.check_yum(files, rpms)
if problems.length() > 0 {
raise Failed("the yum index is inconsistent: \{problems.join("; ")}")
}
match signer {
None =>
println(
"yum: \{rpms.length()} package(s); use a .repo file with baseurl=file:// and gpgcheck=0",
)
Some((key, ring)) => {
let signed = @repo.sign_yum(files, key, created=stamp) catch {
err => raise Failed("\{err}")
}
write_index(dir, signed)
let bad = @repo.check_yum_signature(files + signed, ring)
if bad.length() > 0 {
raise Failed("the yum signature does not verify: \{bad.join("; ")}")
}
println(
"yum: \{rpms.length()} package(s) signed by \{key.fingerprint()}; use a .repo file with repo_gpgcheck=1 and gpgkey=",
)
}
}
}
if archs.length() > 0 {
let name = match options.value("name") {
Some(name) => name
None => "linuxpkg"
}
let signatures = package_files(dir, ".sig")
let files = @repo.arch_index(archs, signatures, name~, date=stamp) catch {
err => raise Failed("\{err}")
}
write_index(dir, files)
let problems = @repo.check_arch(files, archs, signatures, name~)
if problems.length() > 0 {
raise Failed(
"the pacman database is inconsistent: \{problems.join("; ")}",
)
}
match signer {
None =>
println(
"arch: \{archs.length()} package(s); use [\{name}] with Server = file:// and SigLevel = Optional TrustAll",
)
Some((key, ring)) => {
let signed = @repo.sign_arch(files, key, created=stamp) catch {
err => raise Failed("\{err}")
}
write_index(dir, signed)
let bad = @repo.check_arch_signature(files + signed, ring, name~)
if bad.length() > 0 {
raise Failed("the pacman signatures do not verify: \{bad.join("; ")}")
}
println(
"arch: \{archs.length()} package(s) signed by \{key.fingerprint()}; use [\{name}] with Server = file:// and SigLevel = Required DatabaseRequired",
)
}
}
}
if apks.length() > 0 {
let description = match options.value("name") {
Some(name) => name
None => "linuxpkg"
}
let files = @repo.apk_index(apks, description~, date=stamp) catch {
err => raise Failed("\{err}")
}
let problems = @repo.check_apk(files, apks)
if problems.length() > 0 {
raise Failed("the apk index is inconsistent: \{problems.join("; ")}")
}
match signer {
None => {
write_index(dir, files)
println(
"apk: \{apks.length()} package(s); use `apk --repository --allow-untrusted add ...`",
)
}
Some((key, _)) if !key.is_rsa() =>
if format == "apk" {
raise Failed("Alpine repositories can only be signed with an RSA key")
} else {
eprintln(
"linuxpkg: the apk repository is not written: Alpine only supports RSA keys",
)
}
Some((key, ring)) => {
let key_name = apk_key_name(key, options.value("apk_key_name"))
let signed = @repo.sign_apk(files, key, key_name~, created=stamp) catch {
err => raise Failed("\{err}")
}
let written = files.map(f => {
match signed.filter(s => s.0 == f.0) {
[s] => s
_ => f
}
})
write_index(dir, written)
let bad = @repo.check_apk_signature(written, ring)
if bad.length() > 0 {
raise Failed("the apk signatures do not verify: \{bad.join("; ")}")
}
println(
"apk: \{apks.length()} package(s) signed by \{key.fingerprint()}; put the output of `linuxpkg key apk-pub` as \{key_name} in /etc/apk/keys/ and use `apk --repository add ...`",
)
}
}
}
0
}