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, /// 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, } #[derive(Deserialize, Debug)] struct Config { package: Package, #[serde(default)] bin: Vec, } 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 { // 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 { 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( 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::>() .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 { 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) }