mirror of
https://github.com/fosskers/cargo-aur.git
synced 2026-07-24 05:44:04 -05:00
356 lines
10 KiB
Rust
356 lines
10 KiB
Rust
mod error;
|
|
|
|
use crate::error::Error;
|
|
use cargo_aur::{GitHost, Package};
|
|
use colored::*;
|
|
use gumdrop::{Options, ParsingStyle};
|
|
use hmac_sha256::Hash;
|
|
use serde::Deserialize;
|
|
use std::fs::{DirEntry, File};
|
|
use std::io::{BufWriter, Write};
|
|
use std::ops::Not;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, ExitCode};
|
|
|
|
/// Licenses available from the Arch Linux `licenses` package.
|
|
///
|
|
/// That package contains other licenses, but I've excluded here those unlikely
|
|
/// to be used by Rust crates.
|
|
const LICENSES: &[&str] = &[
|
|
"AGPL-3.0-only",
|
|
"AGPL-3.0-or-later",
|
|
"Apache-2.0",
|
|
"BSL-1.0", // Boost Software License.
|
|
"GPL-2.0-only",
|
|
"GPL-2.0-or-later",
|
|
"GPL-3.0-only",
|
|
"GPL-3.0-or-later",
|
|
"LGPL-2.0-only",
|
|
"LGPL-2.0-or-later",
|
|
"LGPL-3.0-only",
|
|
"LGPL-3.0-or-later",
|
|
"MPL-2.0", // Mozilla Public License.
|
|
"Unlicense", // Not to be confused with "Unlicensed".
|
|
];
|
|
|
|
#[derive(Options)]
|
|
struct Args {
|
|
/// Display this help message.
|
|
help: bool,
|
|
/// Display the current version of this software.
|
|
version: bool,
|
|
/// Set a custom output directory (default: target/).
|
|
output: Option<PathBuf>,
|
|
/// Use the MUSL build target to produce a static binary.
|
|
musl: bool,
|
|
/// Don't actually build anything.
|
|
dryrun: bool,
|
|
/// Absorbs any extra junk arguments.
|
|
#[options(free)]
|
|
free: Vec<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct Config {
|
|
package: Package,
|
|
#[serde(default)]
|
|
bin: Vec<Binary>,
|
|
}
|
|
|
|
impl Config {
|
|
/// The name of the compiled binary that should be copied to the tarball.
|
|
fn binary_name(&self) -> &str {
|
|
self.bin
|
|
.first()
|
|
.map(|bin| bin.name.as_str())
|
|
.unwrap_or(self.package.name.as_str())
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct Binary {
|
|
name: String,
|
|
}
|
|
|
|
fn main() -> ExitCode {
|
|
let args = Args::parse_args_or_exit(ParsingStyle::AllOptions);
|
|
|
|
if args.version {
|
|
let version = env!("CARGO_PKG_VERSION");
|
|
println!("{}", version);
|
|
ExitCode::SUCCESS
|
|
} else if let Err(e) = work(args) {
|
|
eprintln!("{} {}: {}", "::".bold(), "Error".bold().red(), e);
|
|
ExitCode::FAILURE
|
|
} else {
|
|
println!("{} {}", "::".bold(), "Done.".bold().green());
|
|
ExitCode::SUCCESS
|
|
}
|
|
}
|
|
|
|
fn work(args: Args) -> Result<(), Error> {
|
|
// We can't proceed if the user has specified `--musl` but doesn't have the
|
|
// target installed.
|
|
if args.musl {
|
|
p("Checking for musl toolchain...".bold());
|
|
musl_check()?
|
|
}
|
|
|
|
// Where cargo expects to read and write to. By default we want to read the
|
|
// built binary from `target/release` and we want to write our results to
|
|
// `target/cargo-aur`, but these are configurable by the user.
|
|
let cargo_target: PathBuf = match std::env::var_os("CARGO_TARGET_DIR") {
|
|
Some(p) => PathBuf::from(p),
|
|
None => PathBuf::from("target"),
|
|
};
|
|
|
|
let output = args.output.unwrap_or(cargo_target.join("cargo-aur"));
|
|
|
|
// Ensure the target can actually be written to. Otherwise the `tar`
|
|
// operation later on will fail.
|
|
std::fs::create_dir_all(&output)?;
|
|
|
|
let config = cargo_config()?;
|
|
|
|
// Warn if the user if still using the old metadata definition style.
|
|
if let Some(metadata) = config.package.metadata.as_ref() {
|
|
if metadata.depends.is_empty().not() || metadata.optdepends.is_empty().not() {
|
|
p("Use of [package.metadata] is deprecated. Please specify extra dependencies under [package.metadata.aur].".bold().yellow());
|
|
}
|
|
}
|
|
|
|
let license = if must_copy_license(&config.package.license) {
|
|
p("LICENSE file will be installed manually.".bold().yellow());
|
|
Some(license_file()?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if args.dryrun.not() {
|
|
release_build(args.musl)?;
|
|
tarball(args.musl, &cargo_target, &output, license.as_ref(), &config)?;
|
|
let sha256: String = sha256sum(&config.package, &output)?;
|
|
|
|
// Write the PKGBUILD.
|
|
let path = output.join("PKGBUILD");
|
|
let file = BufWriter::new(File::create(path)?);
|
|
pkgbuild(file, &config, &sha256, license.as_ref())?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Read the `Cargo.toml` for all the fields of concern to this tool.
|
|
fn cargo_config() -> Result<Config, Error> {
|
|
// NOTE 2023-11-27 Yes it looks silly to be reading the whole thing into a
|
|
// string here, but the `toml` library doesn't allow deserialization from
|
|
// anything else but a string.
|
|
let content = std::fs::read_to_string("Cargo.toml")?;
|
|
let proj: Config = toml::from_str(&content)?;
|
|
Ok(proj)
|
|
}
|
|
|
|
/// If a AUR package's license isn't included in `/usr/share/licenses/common/`,
|
|
/// then it must be installed manually by the PKGBUILD. MIT and BSD3 are such
|
|
/// missing licenses, and since many Rust crates use them we must make this
|
|
/// check.
|
|
fn must_copy_license(license: &str) -> bool {
|
|
LICENSES.contains(&license).not()
|
|
}
|
|
|
|
/// The path to the `LICENSE` file.
|
|
fn license_file() -> Result<DirEntry, Error> {
|
|
std::fs::read_dir(".")?
|
|
.filter_map(|entry| entry.ok())
|
|
.find(|entry| {
|
|
entry
|
|
.file_name()
|
|
.to_str()
|
|
.map(|s| s.starts_with("LICENSE"))
|
|
.unwrap_or(false)
|
|
})
|
|
.ok_or(Error::MissingLicense)
|
|
}
|
|
|
|
/// Write a legal PKGBUILD to some `Write` instance (a `File` in this case).
|
|
fn pkgbuild<T>(
|
|
mut file: T,
|
|
config: &Config,
|
|
sha256: &str,
|
|
license: Option<&DirEntry>,
|
|
) -> Result<(), Error>
|
|
where
|
|
T: Write,
|
|
{
|
|
let package = &config.package;
|
|
let authors = package
|
|
.authors
|
|
.iter()
|
|
.map(|a| format!("# Maintainer: {}", a))
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
let source = package
|
|
.git_host()
|
|
.unwrap_or(GitHost::Github)
|
|
.source(&config.package);
|
|
|
|
writeln!(file, "{}", authors)?;
|
|
writeln!(file, "#")?;
|
|
writeln!(
|
|
file,
|
|
"# This PKGBUILD was generated by `cargo aur`: https://crates.io/crates/cargo-aur"
|
|
)?;
|
|
writeln!(file)?;
|
|
writeln!(file, "pkgname={}-bin", package.name)?;
|
|
writeln!(file, "pkgver={}", package.version)?;
|
|
writeln!(file, "pkgrel=1")?;
|
|
writeln!(file, "pkgdesc=\"{}\"", package.description)?;
|
|
writeln!(file, "url=\"{}\"", package.url())?;
|
|
writeln!(file, "license=(\"{}\")", package.license)?;
|
|
writeln!(file, "arch=(\"x86_64\")")?;
|
|
writeln!(file, "provides=(\"{}\")", package.name)?;
|
|
writeln!(file, "conflicts=(\"{}\")", package.name)?;
|
|
|
|
match package.metadata.as_ref() {
|
|
Some(metadata) if metadata.non_empty() => {
|
|
writeln!(file, "{}", metadata)?;
|
|
}
|
|
Some(_) | None => {}
|
|
}
|
|
|
|
writeln!(file, "source=(\"{}\")", source)?;
|
|
writeln!(file, "sha256sums=(\"{}\")", sha256)?;
|
|
writeln!(file)?;
|
|
writeln!(file, "package() {{")?;
|
|
writeln!(
|
|
file,
|
|
" install -Dm755 {} -t \"$pkgdir/usr/bin\"",
|
|
config.binary_name()
|
|
)?;
|
|
|
|
if let Some(lic) = license {
|
|
let file_name = lic
|
|
.file_name()
|
|
.into_string()
|
|
.map_err(|_| Error::Utf8OsString)?;
|
|
writeln!(
|
|
file,
|
|
" install -Dm644 {} \"$pkgdir/usr/share/licenses/$pkgname/{}\"",
|
|
file_name, file_name
|
|
)?;
|
|
}
|
|
|
|
if let Some(aur) = package.metadata.as_ref().and_then(|m| m.aur.as_ref()) {
|
|
for (source, target) in aur.files.iter() {
|
|
if target.has_root().not() {
|
|
return Err(Error::TargetNotAbsolute(target.to_path_buf()));
|
|
} else {
|
|
writeln!(
|
|
file,
|
|
" install -Dm644 \"{}\" \"$pkgdir{}\"",
|
|
source.display(),
|
|
target.display()
|
|
)?;
|
|
}
|
|
}
|
|
|
|
for custom in aur.custom.iter() {
|
|
writeln!(file, " {}", custom)?;
|
|
}
|
|
}
|
|
|
|
writeln!(file, "}}")?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Run `cargo build --release`, potentially building statically.
|
|
fn release_build(musl: bool) -> Result<(), Error> {
|
|
let mut args = vec!["build", "--release"];
|
|
|
|
if musl {
|
|
args.push("--target=x86_64-unknown-linux-musl");
|
|
}
|
|
|
|
p("Running release build...".bold());
|
|
Command::new("cargo").args(args).status()?;
|
|
Ok(())
|
|
}
|
|
|
|
fn tarball(
|
|
musl: bool,
|
|
cargo_target: &Path,
|
|
output: &Path,
|
|
license: Option<&DirEntry>,
|
|
config: &Config,
|
|
) -> Result<(), Error> {
|
|
let release_dir = if musl {
|
|
"x86_64-unknown-linux-musl/release"
|
|
} else {
|
|
"release"
|
|
};
|
|
|
|
let binary_name = config.binary_name();
|
|
let binary = cargo_target.join(release_dir).join(binary_name);
|
|
|
|
strip(&binary)?;
|
|
std::fs::copy(binary, binary_name)?;
|
|
|
|
// Create the tarball.
|
|
p("Packing tarball...".bold());
|
|
let mut command = Command::new("tar");
|
|
command
|
|
.arg("czf")
|
|
.arg(config.package.tarball(output))
|
|
.arg(binary_name);
|
|
if let Some(lic) = license {
|
|
command.arg(lic.path());
|
|
}
|
|
if let Some(files) = config
|
|
.package
|
|
.metadata
|
|
.as_ref()
|
|
.and_then(|m| m.aur.as_ref())
|
|
.map(|a| a.files.as_slice())
|
|
{
|
|
for (file, _) in files {
|
|
command.arg(file);
|
|
}
|
|
}
|
|
command.status()?;
|
|
|
|
std::fs::remove_file(binary_name)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Strip the release binary, so that we aren't compressing more bytes than we
|
|
/// need to.
|
|
fn strip(path: &Path) -> Result<(), Error> {
|
|
p("Stripping binary...".bold());
|
|
Command::new("strip").arg(path).status()?;
|
|
Ok(()) // FIXME Would love to use my `void` package here and elsewhere.
|
|
}
|
|
|
|
fn sha256sum(package: &Package, output: &Path) -> Result<String, Error> {
|
|
let bytes = std::fs::read(package.tarball(output))?;
|
|
let digest = Hash::hash(&bytes);
|
|
let hex = digest.iter().map(|u| format!("{:02x}", u)).collect();
|
|
Ok(hex)
|
|
}
|
|
|
|
/// Does the user have the `x86_64-unknown-linux-musl` target installed?
|
|
fn musl_check() -> Result<(), Error> {
|
|
let args = ["target", "list", "--installed"];
|
|
let output = Command::new("rustup").args(args).output()?.stdout;
|
|
|
|
std::str::from_utf8(&output)?
|
|
.lines()
|
|
.any(|tc| tc == "x86_64-unknown-linux-musl")
|
|
.then_some(())
|
|
.ok_or(Error::MissingMuslTarget)
|
|
}
|
|
|
|
fn p(msg: ColoredString) {
|
|
println!("{} {}", "::".bold(), msg)
|
|
}
|