From 6b6b1f1d7fa0900a2f3a7ae505df5d95c6e2deb0 Mon Sep 17 00:00:00 2001 From: Arthur Beck Date: Sat, 21 Dec 2024 15:02:29 -0600 Subject: [PATCH] documenting and testing, still lots to do --- Cargo.toml | 2 +- rust-config.toml | 2 +- src/copied.rs | 48 +++++++-- src/lib.rs | 171 ++++++++++++++++++++++++++++--- src/main.rs | 15 ++- src/resources.rs | 10 ++ src/targets.rs | 10 ++ src/template/rust-toolchain.toml | 3 +- src/tests.rs | 64 ++++++++++++ 9 files changed, 296 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2f5a8c9..79d22c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "rust-pkg-gen" version = "0.1.0" edition = "2021" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" repository = "https://github.com/AverseABFun/rust-pkg-gen" homepage = "https://github.com/AverseABFun/rust-pkg-gen" authors = ["Arthur Beck "] diff --git a/rust-config.toml b/rust-config.toml index 86531a9..806b4ee 100644 --- a/rust-config.toml +++ b/rust-config.toml @@ -1,6 +1,6 @@ [x64_package_linux_rust_pkg_gen] toolchains = [ - { edition = "2021", channel = "nightly", profile = "complete", components = [ + { edition = "2021", channel = "nightly", components = [ "rustfmt", "rustc", "cargo", diff --git a/src/copied.rs b/src/copied.rs index c536034..18ed684 100644 --- a/src/copied.rs +++ b/src/copied.rs @@ -1,6 +1,9 @@ -// Copied from crate rustup-mirror(https://github.com/jiegec/rustup-mirror) -// Some modifications made due to deprecated/changed APIs -// (and differing purposes/necessities) +//! Copied from crate [rustup-mirror](https://crates.io/crates/rustup-mirror/0.8.1) +//! Some modifications made due to deprecated/changed APIs +//! (and differing purposes/necessities). +//! +//! Note that I(the maintainer of rust-pkg-gen) wrote the doc comments, NOT +//! the maintainer of [rustup-mirror](https://crates.io/crates/rustup-mirror/0.8.1). use anyhow::{anyhow, Error}; use filebuffer::FileBuffer; @@ -12,8 +15,11 @@ use std::path::{Component, Path, PathBuf}; use toml::Value; use url::Url; +/// The default upstream URL. Usually passed to [`download`] or [`download_all`] +/// when you don't have a custom upstream url to use. pub const DEFAULT_UPSTREAM_URL: &str = "https://static.rust-lang.org/"; +/// Produces the SHA256 hash of the provided file fn file_sha256(file_path: &Path) -> Option { let file = Path::new(file_path); if file.exists() { @@ -24,6 +30,7 @@ fn file_sha256(file_path: &Path) -> Option { } } +/// Download a path from the provided upstream URL. fn download(upstream_url: &str, dir: &str, path: &str) -> Result { println!("Downloading file {}...", path); let manifest = format!("{}{}", upstream_url, path); @@ -50,6 +57,7 @@ fn download(upstream_url: &str, dir: &str, path: &str) -> Result Ok(mirror.join(path)) } +/// I'm honestly unsure what this one does. If you know, please submit an issue or PR! pub fn normalize_path(path: &Path) -> PathBuf { let mut components = path.components().peekable(); let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { @@ -77,6 +85,13 @@ pub fn normalize_path(path: &Path) -> PathBuf { ret } +/// This is the beefy function. It takes an absurd number of arguments +/// and based on them downloads a certain subset of the rust components +/// that are relevant. +/// +/// I changed this one from the original crate a *lot*. This is based +/// on part of the main function in the original crate with many more +/// validations and miscellaneous changes. pub fn download_all( channels: Vec<&str>, upstream_url: &str, @@ -87,22 +102,40 @@ pub fn download_all( for_targets: Vec<&str>, quiet: bool, format_map: HashMap<&str, Vec>, -) { +) -> Option { for channel in channels.clone() { if !crate::targets::RELEASE_CHANNELS.contains(&channel) { - return; + return Some(anyhow!("invalid channel")); } } for target in targets.clone() { if !crate::targets::TARGETS.contains(&target) { - return; + return Some(anyhow!("invalid rust target")); } } for target in for_targets.clone() { if !crate::targets::TARGETS.contains(&target) { - return; + return Some(anyhow!("invalid compilation target")); } } + for (target, formats) in format_map { + if !targets.contains(&target) { + return Some(anyhow!("target that is not being built for in target map")); + } + for format in formats { + if !vec![ + String::from("msi"), + String::from("pkg"), + String::from("gz"), + String::from("xz"), + ] + .contains(&format.format) + { + return Some(anyhow!("invalid format {}", format.format)); + } + } + } + let mut all_targets = HashSet::new(); // All referenced files @@ -336,4 +369,5 @@ pub fn download_all( println!("Producing /{}", alt_sha256_new_file_name); } } + None } diff --git a/src/lib.rs b/src/lib.rs index de9d81f..a94c8f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,27 +1,52 @@ +#![warn(missing_docs)] + +//! Main file for the crate. Contains most of the public API, +//! outside of mainly the code copied from [rustup-mirror](https://github.com/jiegec/rustup-mirror) +//! and the resources included in the output. + +use anyhow::{anyhow, Error}; use serde::Deserialize; use std::{collections::HashMap, fs, path::Path}; pub mod copied; pub mod resources; pub mod targets; -pub mod tests; +#[cfg(test)] +mod tests; #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] +/// Contains all relevant information for a toolchain +/// +/// `rust-config.toml` contains a list of these under `toolchains` +/// in each config. pub struct Toolchain { + /// The edition of rust to use. Should be one of 2015, 2018, 2021, or 2024, + /// but isn't directly validated. Validation could be changed in the future. pub edition: String, + /// The channel of rust to use. Should be in [`targets::RELEASE_CHANNELS`]. pub channel: String, - pub profile: String, + /// The components of rust to install. pub components: Vec, + /// The ID used to index into the [`Crates`] instance associated with the + /// rust config(technically [`RustConfigInner`], but whatever). pub crate_id: String, + /// The list of targets to provide the rust components for. pub platforms: Vec, + /// The list of targets to allow the [`platforms`](Toolchain::platforms) to build for. pub targets: Vec, + /// A map of [`platforms`](Toolchain::platforms) to format IDs. Format IDs are used to + /// index into the [rust config's format list](RustConfigInner::formats). pub format_map: HashMap, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] +/// The suffix used for [Formats](Format). pub enum Suffix { + /// Use the format if it's available, + /// but continue if it's not. IfAvailable, + /// Use the provided format or stop building the package. Only, } @@ -46,47 +71,153 @@ impl<'de> serde::de::Visitor<'de> for StringVisitor { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] +/// A format that includes one of the allowed [formats](FORMATS) +/// and a [`Suffix`]. pub struct Format { + /// The actual format. One of [`FORMATS`]. pub format: String, + /// The suffix. See [the type documentation](Suffix) for information about the valid values. pub suffix: Suffix, } -impl<'de> Deserialize<'de> for Format { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let base_string = deserializer.deserialize_str(StringVisitor)?; +/// Slice of the formats that can be used in a [`Format`]. +pub const FORMATS: [&str; 4] = ["msi", "pkg", "gz", "xz"]; + +impl Format { + /// this function is a basic wrapper around and thus + /// has the same semantic meaning as [`Format::from_string`] + pub fn from_str(base_string: &str) -> Result { + Format::from_string(base_string.to_string()) + } + /// this function is a basic wrapper around and thus + /// has the same semantic meaning as [`Format::from_string_no_err`] + pub fn from_str_no_err(base_string: &str) -> Format { + Format::from_string_no_err(base_string.to_string()) + } + /// this function takes in a `String` and produces a [`Format`] if it's + /// valid. if not, it returns an [`anyhow::Error`]. + /// + /// any valid format has to match the regex: + /// + /// `(?:(?:msi)|(?:pkg)|(?:gz)|(?:xz))(?:(?:-if-available)|(?:-only))?` + /// + /// so `msi` or `gz-only` are valid but `dfsjj-only` isn't + /// + /// (the mentioned regex is not internally used) + /// + /// [`FORMATS`] is a slice of valid formats(msi, pkg, gz, and xz) + /// + /// you can use [`Format::from_str`] if you want to convert a `&str` + /// to a Format, or [`Format::from_string_no_err`] if your application + /// requires that there not be an error case. + /// + /// the reason you wouldn't just call [`Result::unwrap`] to remove the + /// error case is that `from_string_no_err` also allows cases that + /// this method would not. (for example, `from_string_no_err` + /// allows the aforementioned case of `dfsjj-only` or even + /// `dfsjj-ndfdsdf`) + pub fn from_string(base_string: String) -> Result { let split = base_string.split_once("-").unwrap_or((&base_string, "")); let suffix = split.1.to_string(); let real_suffix = match suffix.as_str() { - "-only" => Suffix::Only, - _ => Suffix::IfAvailable, + "only" => Suffix::Only, + "" | "if-available" => Suffix::IfAvailable, + _ => return Err(anyhow!("invalid suffix {}", suffix)), }; + if !FORMATS.contains(&split.0) { + return Err(anyhow!("invalid format {}", split.0)); + } Ok(Format { format: split.0.to_string(), suffix: real_suffix, }) } + /// see [`Format::from_string`] for the usage, this is virtually + /// identical however doesn't return a `Result`. + /// + /// note that this method will report a value for any string(so anything + /// that matches `.*`) but will divide on the first occurance of `-`. + /// see the following code: + /// + /// ``` + /// # use rust_pkg_gen::{Format,Suffix}; + /// let test_string = "test-ah-yes".to_string(); + /// assert_eq!(Format::from_string_no_err(test_string), + /// Format { + /// format: "test".to_string(), + /// suffix: Suffix::IfAvailable + /// } + /// ); + /// ``` + /// + /// keep this in mind when using this method. thus the regex `([\w&&[^-]]+)((?:-.*)?)` + /// matches the output where capture group one is the format and capture group two + /// is the suffix. however, if the suffix is not "only", then it will output + /// [`Suffix::IfAvailable`]. + /// + /// THIS REGEX MAY OR MAY NOT BE KEPT UP-TO-DATE AS IT IS NOT USED INTERNALLY + /// + /// the reason that this method was implemented was to solve the problem + /// of constructing an error for Format::deserialize. + /// + /// I don't like that we had to implement it, but it's kind of necessary. + /// If you have any ideas, please please please open an issue or PR! + pub fn from_string_no_err(base_string: String) -> Format { + let split = base_string.split_once("-").unwrap_or((&base_string, "")); + let suffix = split.1.to_string(); + let real_suffix = match suffix.as_str() { + "only" => Suffix::Only, + _ => Suffix::IfAvailable, + }; + Format { + format: split.0.to_string(), + suffix: real_suffix, + } + } +} + +impl<'de> Deserialize<'de> for Format { + /// Deserialize this value from the given Serde deserializer. + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Format::from_string_no_err( + deserializer.deserialize_str(StringVisitor)?, + )) + } } #[derive(Deserialize, Debug, Clone)] #[serde(untagged)] +/// A crate(used in [a rust config's crates value](RustConfigInner::crates)). +/// +/// Can be any valid value that can be put in a Cargo.toml's dependency section. +/// +/// Doesn't include the crate's name, which is assumed to be placed elsewhere. pub enum Crate { + /// A basic version. What is generally seen in most Cargo.toml's. Version(String), + /// Detailed information about the dependency. Can include a version, + /// features, a path, and/or a git repository. Detailed { + /// The version. Generally a semver. version: Option, + /// The required features. features: Option>, + /// The path to the crate. path: Option, + /// The git repository of the crate. git: Option, }, } impl Crate { + /// Serializes the [`Crate`] to the standard format used in a Cargo.toml. pub fn serialize(self) -> String { if let Crate::Version(str) = self { - return str; + return format!("\"{}\"", str); } else { let Crate::Detailed { version, @@ -119,17 +250,31 @@ impl Crate { } } +/// Many crates. The key for the outer HashMap is +/// a crate ID, and the key for the inner HashMap +/// is a crate. pub type Crates = HashMap>; #[derive(Deserialize, Debug, Clone)] +/// The actual Rust config. Referred to simply by "Rust config" +/// throughout this documentation. The entrypoint to deserializing +/// a `rust-config.toml` file's individual configs. pub struct RustConfigInner { + /// A list of toolchains. pub toolchains: Vec, + /// A list of crates. pub crates: Crates, + /// A list of formats. pub formats: HashMap>, } +/// A Rust config file. The entrypoint to deserializing a +/// `rust-config.toml` file. pub type RustConfig = HashMap; +/// Parse a `rust-config.toml` file. Simply reads a path and parses it as toml. +/// +/// Currently panics upon an error; May change in the future. pub fn parse_file(path: &Path) -> RustConfig { toml::from_str(&fs::read_to_string(path).unwrap()).unwrap() } diff --git a/src/main.rs b/src/main.rs index 41c4526..2ead7a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,8 @@ +#![warn(missing_docs)] + +//! The binary entrypoint to rust-pkg-gen. Currently contains most of the code, +//! but that will be changed eventually as a non-breaking change. + use clap::Parser; use rand::{Rng, SeedableRng}; use rust_pkg_gen::resources::TemplateAssets; @@ -44,7 +49,7 @@ struct Cli { short = 'q', long = "quiet", default_value_t = false, - help = "Doesn't display any unnecessary text(still shows confirmation prompts; to remove, use --overwrite" + help = "Doesn't display any unnecessary text(still shows confirmation prompts; to remove, use -y --overwrite as well or --silent." )] quiet: bool, #[arg( @@ -67,7 +72,7 @@ fn generate_crates( ) -> String { let mut out: String = String::new(); for (crte, version) in cfg.crates.get(&toolchain.crate_id).unwrap() { - out = format!("{}{} = \"{}\"\n", out, crte, version.clone().serialize()).to_string(); + out = format!("{}{} = {}\n", out, crte, version.clone().serialize()).to_string(); } out } @@ -140,7 +145,6 @@ fn main() { .unwrap() .replace("{?TOOLCHAIN.EDITION}", &toolchain.edition) .replace("{?TOOLCHAIN.CHANNEL}", &toolchain.channel) - .replace("{?TOOLCHAIN.PROFILE}", &toolchain.profile) .replace( "{?TOOLCHAIN.TARGETS}", &("\"".to_owned() + &toolchain.targets.join("\",\"") + "\""), @@ -176,7 +180,7 @@ fn main() { write( dir.join(PathBuf::from("crates")) .join(PathBuf::from("README.md")), - include_str!("crates_README.md").replace( + rust_pkg_gen::resources::CRATES_README.replace( "{?TOOLCHAIN.CRATES_DIR}", fs::canonicalize(dir.join(PathBuf::from("crates"))) .unwrap() @@ -198,7 +202,8 @@ fn main() { toolchain .format_map .iter() - .map(|(k, v)| (k, cfg.formats[v])), + .map(|(k, v)| (k.as_str(), cfg.formats[v].clone())) + .collect(), ); if !args.save_temp { diff --git a/src/resources.rs b/src/resources.rs index 3d3cf06..18e7816 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,6 +1,16 @@ +//! Resources used in the produced output. + use rust_embed::Embed; #[derive(Embed)] #[folder = "src/template/"] #[prefix = "template/"] +/// The template assets outputted to {temp_dir}/{target}/crates. +/// +/// If viewing after running, it will be the output of [`cargo-local-registry`](https://crates.io/crates/cargo-local-registry/0.2.7) +/// (build alongside this crate in an incredibly janky way) pub struct TemplateAssets; + +/// The README file automatically placed into {temp_dir}/{target}/crates +/// after [`cargo-local-registry`](https://crates.io/crates/cargo-local-registry/0.2.7) runs. +pub const CRATES_README: &str = include_str!("crates_README.md"); diff --git a/src/targets.rs b/src/targets.rs index b213a9e..8a9591a 100644 --- a/src/targets.rs +++ b/src/targets.rs @@ -1,5 +1,15 @@ +//! Contains all valid targets and channels. Copied and modified from [rustup-mirror](https://crates.io/crates/rustup-mirror/0.8.1). + +/// Valid rust channels. Currently only stable, beta, and nightly. pub const RELEASE_CHANNELS: [&str; 3] = ["stable", "beta", "nightly"]; +/// List of valid rust targets. According to rustup-mirror, can be generated with: +/// ```bash +/// rustc --print target-list | awk '{print " \"" $1 "\","}' +/// ``` +/// Ensure to rerun on nightly regularly! +/// (if anyone has any better ideas that doesn't require remembering to run a +/// command manually, please submit an issue or PR) pub const TARGETS: [&str; 271] = [ "aarch64-apple-darwin", "aarch64-apple-ios", diff --git a/src/template/rust-toolchain.toml b/src/template/rust-toolchain.toml index a61d9db..20eaaef 100644 --- a/src/template/rust-toolchain.toml +++ b/src/template/rust-toolchain.toml @@ -1,5 +1,4 @@ [toolchain] channel = "{?TOOLCHAIN.CHANNEL}" components = [ {?TOOLCHAIN.COMPONENTS} ] -targets = [ {?TOOLCHAIN.TARGETS} ] -profile = "{?TOOLCHAIN.PROFILE}" \ No newline at end of file +targets = [ {?TOOLCHAIN.TARGETS} ] \ No newline at end of file diff --git a/src/tests.rs b/src/tests.rs index 8b13789..59d5cdd 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1 +1,65 @@ +use super::*; +use proptest::prelude::*; +#[test] +fn format_from_string() { + // this covers all valid cases but not invalid cases + for ele in FORMATS { + assert!(Format::from_str(ele).is_ok()); + assert!(Format::from_string(ele.to_string() + "-if-available").is_ok()); + assert!(Format::from_string(ele.to_string() + "-only").is_ok()); + + assert_eq!( + Format::from_str_no_err(ele), + Format { + format: ele.to_string(), + suffix: Suffix::IfAvailable + } + ); + assert_eq!( + Format::from_str(ele).unwrap(), + Format { + format: ele.to_string(), + suffix: Suffix::IfAvailable + } + ); + + assert_eq!( + Format::from_string_no_err(ele.to_string() + "-if-available"), + Format { + format: ele.to_string(), + suffix: Suffix::IfAvailable + } + ); + assert_eq!( + Format::from_string(ele.to_string() + "-if-available").unwrap(), + Format { + format: ele.to_string(), + suffix: Suffix::IfAvailable + } + ); + + assert_eq!( + Format::from_string_no_err(ele.to_string() + "-only"), + Format { + format: ele.to_string(), + suffix: Suffix::Only + } + ); + assert_eq!( + Format::from_string(ele.to_string() + "-only").unwrap(), + Format { + format: ele.to_string(), + suffix: Suffix::Only + } + ); + } +} + +proptest! { + #[test] + fn format_from_string_handles_utf8(s in "\\PC*") { + Format::from_string_no_err(s.clone()); + let _ = Format::from_string(s); + } +}