agb/tools/src/publish.rs

256 lines
7.5 KiB
Rust
Raw Normal View History

2022-08-05 06:36:24 +10:00
use clap::{Arg, ArgAction, ArgMatches};
2022-08-05 07:17:54 +10:00
use std::collections::{HashMap, HashSet};
2022-08-05 06:11:44 +10:00
use std::fs;
2022-08-05 06:10:11 +10:00
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use std::{env, thread};
use toml_edit::Document;
#[derive(Debug)]
pub enum Error {
FindRootDirectory,
PublishCrate,
Poll,
CrateVersion,
ReadingDependencies,
CargoToml,
2022-08-05 06:10:11 +10:00
}
2022-10-02 05:42:39 +11:00
pub fn command() -> clap::Command {
2022-08-05 06:36:24 +10:00
clap::Command::new("publish")
.about("Publishes agb and all subcrates")
.arg(
Arg::new("Dry run")
.long("dry-run")
.help("Don't actually publish")
.action(ArgAction::SetTrue),
)
}
pub fn publish(matches: &ArgMatches) -> Result<(), Error> {
let dry_run = matches.get_one::<bool>("Dry run").expect("defined by clap");
2022-08-05 06:10:11 +10:00
let root_directory = find_agb_root_directory()?;
2022-08-05 07:17:54 +10:00
let mut fully_published_crates: HashSet<String> = HashSet::new();
let mut published_crates: HashSet<String> = HashSet::new();
let dependencies = build_dependency_graph(&root_directory)?;
let crates_to_publish: Vec<_> = dependencies.keys().collect();
2022-08-05 07:17:54 +10:00
while published_crates.len() != crates_to_publish.len() {
2022-08-05 07:17:54 +10:00
// find all crates which can be published now but haven't
let publishable_crates: Vec<_> = crates_to_publish
2022-08-05 07:17:54 +10:00
.iter()
.filter(|&&crate_to_publish| !published_crates.contains(crate_to_publish))
.filter(|&&crate_to_publish| {
let dependencies_of_crate = &dependencies[crate_to_publish];
for dependency_of_crate in dependencies_of_crate {
if !fully_published_crates.contains(dependency_of_crate) {
return false;
}
}
true
})
.collect();
for publishable_crate in publishable_crates {
if *dry_run {
println!("Would execute cargo publish for {publishable_crate}");
} else {
Command::new("cargo")
.arg("publish")
.current_dir(&root_directory.join(publishable_crate))
.spawn()
.map_err(|_| Error::PublishCrate)?;
}
published_crates.insert(publishable_crate.to_string());
}
for published_crate in published_crates.iter() {
if !fully_published_crates.contains(published_crate) {
let expected_version =
read_cargo_toml_version(&root_directory.join(published_crate))?;
if check_if_released(published_crate, &expected_version)? {
fully_published_crates.insert(published_crate.clone());
}
2022-08-05 06:36:24 +10:00
}
2022-08-05 06:10:11 +10:00
}
2022-08-05 07:17:54 +10:00
thread::sleep(Duration::from_secs(10));
2022-08-05 06:10:11 +10:00
}
Ok(())
}
fn find_agb_root_directory() -> Result<PathBuf, Error> {
let mut current_path = env::current_dir().map_err(|_| Error::FindRootDirectory)?;
while !current_path.clone().join("justfile").exists() {
current_path = current_path
.parent()
.ok_or(Error::FindRootDirectory)?
.to_owned();
}
Ok(current_path)
}
2022-08-05 07:17:54 +10:00
fn check_if_released(crate_to_publish: &str, expected_version: &str) -> Result<bool, Error> {
2022-08-05 06:10:11 +10:00
let url_to_poll = &get_url_to_poll(crate_to_publish);
2022-08-05 07:17:54 +10:00
println!("Polling crates.io with URL {url_to_poll} for {crate_to_publish} hoping for version {expected_version}.");
2022-08-05 06:10:11 +10:00
2022-08-05 07:17:54 +10:00
let curl_result = Command::new("curl")
.arg(url_to_poll)
.output()
.map_err(|_| Error::Poll)?;
2022-08-05 06:10:11 +10:00
2022-08-05 07:17:54 +10:00
Ok(String::from_utf8_lossy(&curl_result.stdout).contains(expected_version))
2022-08-05 06:10:11 +10:00
}
fn get_url_to_poll(crate_name: &str) -> String {
let crate_name_with_underscores = crate_name.replace('-', "_");
let crate_folder = if crate_name_with_underscores.len() == 3 {
format!("3/{}", crate_name_with_underscores.chars().next().unwrap())
} else {
let first_two_characters = &crate_name_with_underscores[0..2];
let second_two_characters = &crate_name_with_underscores[2..4];
format!("{first_two_characters}/{second_two_characters}")
};
format!("https://raw.githubusercontent.com/rust-lang/crates.io-index/master/{crate_folder}/{crate_name_with_underscores}")
}
fn read_cargo_toml_version(folder: &Path) -> Result<String, Error> {
let cargo_toml = read_cargo_toml(folder)?;
2022-08-05 06:10:11 +10:00
let version_value = cargo_toml["package"]["version"]
.as_value()
.ok_or(Error::CrateVersion)?
.as_str()
.ok_or(Error::CrateVersion)?;
Ok(version_value.to_owned())
}
fn build_dependency_graph(root: &Path) -> Result<HashMap<String, Vec<String>>, Error> {
2022-08-05 07:17:54 +10:00
let mut result = HashMap::new();
result.insert("agb".to_owned(), get_agb_dependencies(&root.join("agb"))?);
let mut added_new_crates = true;
while added_new_crates {
added_new_crates = false;
let all_crates: HashSet<String> = HashSet::from_iter(result.values().flatten().cloned());
2022-08-05 07:17:54 +10:00
for dep_crate in all_crates {
if result.contains_key(&dep_crate) {
continue;
}
added_new_crates = true;
result.insert(
dep_crate.to_owned(),
get_agb_dependencies(&root.join(dep_crate))?,
);
}
2022-08-05 07:17:54 +10:00
}
Ok(result)
}
fn get_agb_dependencies(folder: &Path) -> Result<Vec<String>, Error> {
let cargo_toml = read_cargo_toml(folder)?;
let dependencies = cargo_toml["dependencies"]
.as_table()
.ok_or(Error::ReadingDependencies)?
.get_values();
let mut result = vec![];
for (key, _) in dependencies {
let dep = key[0].get();
if dep.starts_with("agb") {
result.push(dep.replace('_', "-"))
}
}
Ok(result)
}
fn read_cargo_toml(folder: &Path) -> Result<Document, Error> {
let cargo_toml_contents =
fs::read_to_string(folder.join("Cargo.toml")).map_err(|_| Error::CargoToml)?;
let cargo_toml: Document = cargo_toml_contents.parse().map_err(|_| Error::CargoToml)?;
Ok(cargo_toml)
}
2022-08-05 06:10:11 +10:00
#[cfg(test)]
mod test {
use super::*;
2022-10-02 05:42:39 +11:00
#[test]
fn verify_cli() {
command().debug_assert();
}
2022-08-05 06:10:11 +10:00
#[test]
fn url_to_poll_should_return_correct_url() {
let test_cases = [
["agb", "3/a/agb"],
["agb-image-converter", "ag/b_/agb_image_converter"],
["agb-fixnum", "ag/b_/agb_fixnum"],
];
for [name, result] in test_cases {
let url = get_url_to_poll(name);
assert_eq!(
url,
format!(
"https://raw.githubusercontent.com/rust-lang/crates.io-index/master/{result}",
)
)
}
}
2022-08-05 06:14:47 +10:00
#[test]
fn should_find_root_directory() -> Result<(), Error> {
assert_ne!(find_agb_root_directory()?.to_string_lossy(), "");
Ok(())
}
#[test]
fn should_read_version() -> Result<(), Error> {
let root_directory = find_agb_root_directory()?;
let my_version = read_cargo_toml_version(&root_directory.join("tools"))?;
assert_eq!(my_version, "0.1.0");
Ok(())
}
#[test]
fn should_detect_dependencies() -> Result<(), Error> {
let root_directory = find_agb_root_directory()?;
let deps = get_agb_dependencies(&root_directory.join("agb"))?;
assert_eq!(
deps,
&[
"agb-image-converter",
"agb-sound-converter",
"agb-macros",
"agb-fixnum"
]
);
Ok(())
}
}