pub(crate) mod error;
use crate::error::Error;
use colored::*;
use directories::UserDirs;
use gumdrop::{Options, ParsingStyle};
use hmac_sha256::Hash;
use itertools::Itertools;
use serde_derive::Deserialize;
use std::ffi::OsString;
use std::fs::{DirEntry, File};
use std::io::{BufWriter, Write};
use std::ops::Not;
use std::path::{Path, PathBuf};
use std::process::Command;
const LICENSES: &[&str] = &[
"AGPL3", "APACHE", "GPL2", "GPL3", "LGPL2.1", "LGPL3", "MPL", "MPL2",
];
#[derive(Options)]
struct Args {
help: bool,
version: bool,
#[options(free)]
args: Vec<String>,
musl: bool,
dryrun: bool,
}
enum GitHost {
Github,
Gitlab,
}
impl GitHost {
fn source(&self, package: &Package) -> String {
match self {
GitHost::Github => format!(
"{}/raw/main/{}-$pkgver-x86_64.tar.gz",
package.repository, package.name
),
GitHost::Gitlab => format!(
"{}/-/raw/main/{}-$pkgver-x86_64.tar.gz",
package.repository.replace(".git", ""), package.name
),
}
}
}
#[derive(Deserialize, Debug)]
struct Config {
package: Package,
#[serde(default)]
bin: Vec<Binary>,
}
impl Config {
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 Package {
name: String,
version: String,
authors: Vec<String>,
description: String,
homepage: String,
repository: String,
license: String,
metadata: Option<Metadata>,
}
#[derive(Deserialize, Debug)]
struct Metadata {
#[serde(default)]
depends: Vec<String>,
#[serde(default)]
optdepends: Vec<String>,
}
impl std::fmt::Display for Metadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.depends.as_slice() {
[middle @ .., last] => {
write!(f, "depends=(")?;
for item in middle {
write!(f, "\"{}\" ", item)?;
}
if self.optdepends.is_empty().not() {
writeln!(f, "\"{}\")", last)?;
} else {
write!(f, "\"{}\")", last)?;
}
}
[] => {}
}
match self.optdepends.as_slice() {
[middle @ .., last] => {
write!(f, "optdepends=(")?;
for item in middle {
write!(f, "\"{}\" ", item)?;
}
write!(f, "\"{}\")", last)?;
}
[] => {}
}
Ok(())
}
}
#[derive(Deserialize, Debug)]
struct Binary {
name: String,
}
impl Package {
fn tarball(&self) -> String {
format!("{}-{}-x86_64.tar.gz", self.name, self.version)
}
fn git_host(&self) -> Option<GitHost> {
if self.repository.starts_with("https://github") {
Some(GitHost::Github)
} else if self.repository.starts_with("https://gitlab") {
Some(GitHost::Gitlab)
} else {
None
}
}
}
fn main() {
let args = Args::parse_args_or_exit(ParsingStyle::AllOptions);
if args.version {
let version = env!("CARGO_PKG_VERSION");
println!("{}", version);
} else if let Err(e) = work(args) {
eprintln!("{} {}: {}", "::".bold(), "Error".bold().red(), e);
std::process::exit(1)
} else {
println!("{} Create PKGBUILD {}", "::".bold(), "Done.".bold().green());
if let Ok(_) = doe::system!("makepkg --printsrcinfo > .SRCINFO"){
println!("{} Create .SRCINFO {}", "::".bold(), "Done.".bold().green());
if let Ok(_) = doe::system!("updpkgsums"){
println!("{} updpkgsums {}", "::".bold(), "Done.".bold().green());
}
}
}
}
fn work(args: Args) -> Result<(), Error> {
if args.musl {
p("Checking for musl toolchain...".bold());
musl_check()?
}
let config = cargo_config()?;
let license = if must_copy_license(&config.package.license) {
p("LICENSE file will be installed manually.".bold().yellow());
Some(license_file()?)
} else {
let licence = include_str!("../LICENSE");
std::fs::write("./LICENSE", licence).unwrap();
None
};
if args.dryrun.not() {
release_build(args.musl)?;
tarball(args.musl, license.as_ref(), &config)?;
let sha256: String = sha256sum(&config.package)?;
let file = BufWriter::new(File::create("PKGBUILD")?);
pkgbuild(file, &config, &sha256, license.as_ref())?;
}
Ok(())
}
fn cargo_config() -> Result<Config, Error> {
let content = std::fs::read_to_string("Cargo.toml")?;
let proj: Config = toml::from_str(&content)?;
Ok(proj)
}
fn must_copy_license(license: &str) -> bool {
LICENSES.contains(&license).not()
}
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)
}
fn pkgbuild<T: Write>(
mut file: T,
config: &Config,
sha256: &str,
license: Option<&DirEntry>,
) -> Result<(), Error> {
let package = &config.package;
let authors = package
.authors
.iter()
.map(|a| format!("# Maintainer: {}", a))
.join("\n");
let source = package
.git_host()
.unwrap_or(GitHost::Gitlab)
.source(&config.package);
writeln!(file, "{}", authors)?;
writeln!(file, "#")?;
writeln!(file)?;
writeln!(file, "pkgname={}", package.name)?;
writeln!(file, "pkgver={}", package.version)?;
writeln!(file, "pkgrel=1")?;
writeln!(file, "pkgdesc=\"{}\"", package.description)?;
writeln!(file, "url=\"{}\"", package.homepage)?;
writeln!(file, "license=(\"{}\")", package.license)?;
writeln!(file, "arch=(\"x86_64\")")?;
writeln!(file, "provides=(\"{}\")", package.name)?;
writeln!(file, "conflicts=(\"{}\")", package.name)?;
if let Some(metadata) = package.metadata.as_ref() {
writeln!(file, "{}", metadata)?;
}
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
)?;
}
writeln!(file, "}}")?;
Ok(())
}
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, license: Option<&DirEntry>, config: &Config) -> Result<(), Error> {
let target_dir: OsString = match std::env::var_os("CARGO_TARGET_DIR") {
Some(p) => p,
None => {
let path = PathBuf::from("./target");
if path.is_dir(){
"target".into()
}else{
let user_dirs = UserDirs::new().unwrap();
let home = user_dirs.home_dir();
let cargo_config = format!("{}/.cargo/config",home.to_string_lossy().to_string());
let config_string = std::fs::read_to_string(cargo_config).unwrap();
let cargo_toml = tsu::toml_from_str(config_string);
let build = cargo_toml.get("build").unwrap();
let target_dir = build.get("target-dir").unwrap().as_str().unwrap();
format!("{}/{}",home.to_string_lossy(),target_dir).into()
}
},
};
let release_dir = if musl {
"x86_64-unknown-linux-musl/release"
} else {
"release"
};
let binary_name = config.binary_name();
let mut binary: PathBuf = target_dir.into();
binary.push(release_dir);
binary.push(binary_name);
strip(&binary)?;
std::fs::copy(binary, binary_name)?;
p("Packing tarball...".bold());
let mut command = Command::new("tar");
command
.arg("czf")
.arg(config.package.tarball())
.arg(binary_name);
if let Some(lic) = license {
command.arg(lic.path());
}
command.status()?;
std::fs::remove_file(binary_name)?;
Ok(())
}
fn strip(path: &Path) -> Result<(), Error> {
p("Stripping binary...".bold());
Command::new("strip").arg(path).status()?;
Ok(()) }
fn sha256sum(package: &Package) -> Result<String, Error> {
let bytes = std::fs::read(package.tarball())?;
let digest = Hash::hash(&bytes);
let hex = digest.iter().map(|u| format!("{:02x}", u)).collect();
Ok(hex)
}
fn musl_check() -> Result<(), Error> {
let args = vec!["target", "list", "--installed"];
let output = Command::new("rustup").args(args).output()?.stdout;
let installed = std::str::from_utf8(&output)?
.lines()
.any(|tc| tc == "x86_64-unknown-linux-musl");
if installed {
Ok(())
} else {
Err(Error::MissingTarget)
}
}
fn p(msg: ColoredString) {
println!("{} {}", "::".bold(), msg)
}