diff --git a/Cargo.lock b/Cargo.lock index 07f938928..4d7c3e572 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,17 +190,6 @@ dependencies = [ "unic-langid", ] -[[package]] -name = "anki_i18n_helpers" -version = "0.0.0" -dependencies = [ - "fluent-syntax", - "lazy_static", - "regex", - "serde_json", - "walkdir", -] - [[package]] name = "anki_io" version = "0.0.0" @@ -1295,8 +1284,17 @@ dependencies = [ name = "ftl" version = "0.0.0" dependencies = [ + "anki_io", + "anki_process", + "anyhow", "camino", + "clap", + "fluent-syntax", + "lazy_static", + "regex", + "serde_json", "snafu", + "walkdir", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 226da6d51..7a0c3a79f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" members = [ "rslib", "rslib/i18n", - "rslib/i18n_helpers", "rslib/linkchecker", "rslib/proto", "rslib/io", diff --git a/build/configure/src/rust.rs b/build/configure/src/rust.rs index c4592eab4..4d4b08258 100644 --- a/build/configure/src/rust.rs +++ b/build/configure/src/rust.rs @@ -8,11 +8,11 @@ use ninja_gen::build::FilesHandle; use ninja_gen::cargo::CargoBuild; use ninja_gen::cargo::CargoClippy; use ninja_gen::cargo::CargoFormat; -use ninja_gen::cargo::CargoRun; use ninja_gen::cargo::CargoTest; use ninja_gen::cargo::RustOutput; use ninja_gen::git::SyncSubmodule; use ninja_gen::glob; +use ninja_gen::hash::simple_hash; use ninja_gen::input::BuildInput; use ninja_gen::inputs; use ninja_gen::Build; @@ -60,21 +60,30 @@ fn prepare_translations(build: &mut Build) -> Result<()> { )?; build.add_action( - "ftl:sync", - CargoRun { - binary_name: "ftl-sync", - cargo_args: "-p ftl", - bin_args: "", - deps: inputs![":ftl:repo", glob!["ftl/{core,core-repo,qt,qt-repo}/**"]], + "ftl:bin", + CargoBuild { + inputs: inputs![glob!["ftl/**"],], + outputs: &[RustOutput::Binary("ftl")], + target: None, + extra_args: "-p ftl", + release_override: None, + }, + )?; + + // These don't use :group notation, as it doesn't make sense to invoke multiple + // commands as a group. + build.add_action( + "ftl-sync", + FtlCommand { + args: "sync", + deps: inputs![":ftl:repo", glob!["ftl/**"]], }, )?; build.add_action( - "ftl:deprecate", - CargoRun { - binary_name: "deprecate_ftl_entries", - cargo_args: "-p anki_i18n_helpers", - bin_args: "ftl/core ftl/qt -- pylib qt rslib ts --keep ftl/usage", + "ftl-deprecate", + FtlCommand { + args: "deprecate --ftl-roots ftl/core ftl/qt --source-roots pylib qt rslib ts --json-roots ftl/usage", deps: inputs!["ftl/core", "ftl/qt", "pylib", "qt", "rslib", "ts"], }, )?; @@ -82,6 +91,24 @@ fn prepare_translations(build: &mut Build) -> Result<()> { Ok(()) } +struct FtlCommand { + args: &'static str, + deps: BuildInput, +} + +impl BuildAction for FtlCommand { + fn command(&self) -> &str { + "$ftl_bin $args" + } + + fn files(&mut self, build: &mut impl FilesHandle) { + build.add_inputs("", &self.deps); + build.add_inputs("ftl_bin", inputs![":ftl:bin"]); + build.add_variable("args", self.args); + build.add_output_stamp(format!("ftl/stamp.{}", simple_hash(self.args))); + } +} + fn build_proto_descriptors_and_interfaces(build: &mut Build) -> Result<()> { let outputs = vec![ RustOutput::Data("descriptors.bin", "rslib/proto/descriptors.bin"), diff --git a/ftl/Cargo.toml b/ftl/Cargo.toml index 598e343bc..dae4edfa7 100644 --- a/ftl/Cargo.toml +++ b/ftl/Cargo.toml @@ -6,11 +6,17 @@ edition.workspace = true license.workspace = true publish = false rust-version.workspace = true - -[[bin]] -name = "ftl-sync" -path = "sync.rs" +description = "Helpers for Anki's i18n system" [dependencies] +anki_io.workspace = true +anki_process.workspace = true +anyhow.workspace = true camino.workspace = true +clap.workspace = true +fluent-syntax.workspace = true +lazy_static.workspace = true +regex.workspace = true +serde_json.workspace = true snafu.workspace = true +walkdir.workspace = true diff --git a/rslib/i18n_helpers/src/garbage_collection.rs b/ftl/src/garbage_collection.rs similarity index 83% rename from rslib/i18n_helpers/src/garbage_collection.rs rename to ftl/src/garbage_collection.rs index e51e993c1..5e2f1d2e4 100644 --- a/rslib/i18n_helpers/src/garbage_collection.rs +++ b/ftl/src/garbage_collection.rs @@ -5,7 +5,12 @@ use std::collections::HashSet; use std::fs; use std::io::BufReader; use std::iter::FromIterator; +use std::path::PathBuf; +use anki_io::create_file; +use anyhow::Context; +use anyhow::Result; +use clap::Args; use fluent_syntax::ast; use fluent_syntax::ast::Resource; use fluent_syntax::parser; @@ -17,42 +22,61 @@ use walkdir::WalkDir; use crate::serialize; +#[derive(Args)] +pub struct WriteJsonArgs { + target_filename: PathBuf, + source_roots: Vec, +} + +#[derive(Args)] +pub struct GarbageCollectArgs { + json_root: String, + ftl_roots: Vec, +} + +#[derive(Args)] +pub struct DeprecateEntriesArgs { + #[clap(long, num_args(1..), required(true))] + ftl_roots: Vec, + #[clap(long, num_args(1..), required(true))] + source_roots: Vec, + #[clap(long, num_args(1..), required(true))] + json_roots: Vec, +} + const DEPCRATION_WARNING: &str = "NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future."; /// Extract references from all Rust, Python, TS, Svelte, Swift, Kotlin and /// Designer files in the `roots`, convert them to kebab case and write them as /// a json to the target file. -pub fn write_ftl_json, S2: AsRef>(roots: &[S1], target: S2) { - let refs = gather_ftl_references(roots); +pub fn write_ftl_json(args: WriteJsonArgs) -> Result<()> { + let refs = gather_ftl_references(&args.source_roots); let mut refs = Vec::from_iter(refs); refs.sort(); - serde_json::to_writer_pretty( - fs::File::create(target.as_ref()).expect("failed to create file"), - &refs, - ) - .expect("failed to write file"); + serde_json::to_writer_pretty(create_file(args.target_filename)?, &refs) + .context("writing json")?; + + Ok(()) } /// Delete every entry in `ftl_root` that is not mentioned in another message /// or any json in `json_root`. -pub fn garbage_collect_ftl_entries(ftl_roots: &[impl AsRef], json_root: impl AsRef) { - let used_ftls = get_all_used_messages_and_terms(json_root.as_ref(), ftl_roots); - strip_unused_ftl_messages_and_terms(ftl_roots, &used_ftls); +pub fn garbage_collect_ftl_entries(args: GarbageCollectArgs) -> Result<()> { + let used_ftls = get_all_used_messages_and_terms(&args.json_root, &args.ftl_roots); + strip_unused_ftl_messages_and_terms(&args.ftl_roots, &used_ftls); + Ok(()) } /// Moves every entry in `ftl_roots` that is not mentioned in another message, a /// source file or any json in `json_roots` to the bottom of its file below a /// deprecation warning. -pub fn deprecate_ftl_entries( - ftl_roots: &[impl AsRef], - source_roots: &[impl AsRef], - json_roots: &[impl AsRef], -) { - let mut used_ftls = gather_ftl_references(source_roots); - import_messages_from_json(json_roots, &mut used_ftls); - extract_nested_messages_and_terms(ftl_roots, &mut used_ftls); - deprecate_unused_ftl_messages_and_terms(ftl_roots, &used_ftls); +pub fn deprecate_ftl_entries(args: DeprecateEntriesArgs) -> Result<()> { + let mut used_ftls = gather_ftl_references(&args.source_roots); + import_messages_from_json(&args.json_roots, &mut used_ftls); + extract_nested_messages_and_terms(&args.ftl_roots, &mut used_ftls); + deprecate_unused_ftl_messages_and_terms(&args.ftl_roots, &used_ftls); + Ok(()) } fn get_all_used_messages_and_terms( diff --git a/ftl/src/main.rs b/ftl/src/main.rs new file mode 100644 index 000000000..959e0f7dd --- /dev/null +++ b/ftl/src/main.rs @@ -0,0 +1,50 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +pub mod garbage_collection; +pub mod serialize; + +mod sync; + +use anyhow::Result; +use clap::Parser; +use clap::Subcommand; +use garbage_collection::deprecate_ftl_entries; +use garbage_collection::garbage_collect_ftl_entries; +use garbage_collection::write_ftl_json; +use garbage_collection::DeprecateEntriesArgs; +use garbage_collection::GarbageCollectArgs; +use garbage_collection::WriteJsonArgs; + +#[derive(Parser)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Update commit references to the latest translations, + /// and copy source files to the translation repos. Requires access to the + /// i18n repos to run. + Sync, + /// Extract references from all Rust, Python, TS, Svelte and Designer files + /// in the given roots, convert them to ftl names case and write them as + /// a json to the target file. + WriteJson(WriteJsonArgs), + /// Delete every entry in the ftl files that is not mentioned in another + /// message or a given json. + GarbageCollect(GarbageCollectArgs), + /// Deprecate unused ftl entries by moving them to the bottom of the file + /// and adding a deprecation warning. An entry is considered unused if + /// cannot be found in a source or JSON file. + Deprecate(DeprecateEntriesArgs), +} + +fn main() -> Result<()> { + match Cli::parse().command { + Command::Sync => sync::sync(), + Command::WriteJson(args) => write_ftl_json(args), + Command::GarbageCollect(args) => garbage_collect_ftl_entries(args), + Command::Deprecate(args) => deprecate_ftl_entries(args), + } +} diff --git a/rslib/i18n_helpers/src/serialize.rs b/ftl/src/serialize.rs similarity index 100% rename from rslib/i18n_helpers/src/serialize.rs rename to ftl/src/serialize.rs diff --git a/ftl/sync.rs b/ftl/src/sync.rs similarity index 60% rename from ftl/sync.rs rename to ftl/src/sync.rs index 8461b0303..615b9d03a 100644 --- a/ftl/sync.rs +++ b/ftl/src/sync.rs @@ -1,17 +1,13 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -//! A helper script to update commit references to the latest translations, -//! and copy source files to the translation repos. Requires access to the -//! i18n repos to run. - use std::process::Command; +use anki_process::CommandExt; +use anyhow::bail; +use anyhow::Context; +use anyhow::Result; use camino::Utf8Path; -use snafu::prelude::*; -use snafu::Whatever; - -type Result = std::result::Result; #[derive(Debug)] struct Module { @@ -23,8 +19,7 @@ struct Module { /// remote is used to push via authenticated ssh. const GIT_REMOTE: &str = "ssh"; -#[snafu::report] -fn main() -> Result<()> { +pub fn sync() -> Result<()> { let modules = [ Module { template_folder: "ftl/core".into(), @@ -41,8 +36,7 @@ fn main() -> Result<()> { fetch_new_translations(&module)?; push_new_templates(&module)?; } - commit(".", "Update translations") - .whatever_context("failure expected if no translations changed")?; + commit(".", "Update translations").context("failure expected if no translations changed")?; Ok(()) } @@ -50,47 +44,41 @@ fn check_clean() -> Result<()> { let output = Command::new("git") .arg("diff") .output() - .whatever_context("git diff")?; - ensure_whatever!(output.status.success(), "git diff"); - ensure_whatever!( - output.stdout.is_empty(), - "please commit any outstanding changes first" - ); - Ok(()) -} - -fn run(command: &mut Command) -> Result<()> { - let status = command - .status() - .with_whatever_context(|_| format!("{:?}", command))?; - if !status.success() { - whatever!("{:?} exited with code: {:?}", command, status.code()); + .context("git diff")?; + if !output.status.success() { + bail!("git diff"); + } + if !output.stdout.is_empty() { + bail!("please commit any outstanding changes first"); } Ok(()) } fn fetch_new_translations(module: &Module) -> Result<()> { - run(Command::new("git") + Command::new("git") .current_dir(module.translation_repo) - .args(["checkout", "main"]))?; - run(Command::new("git") + .args(["checkout", "main"]) + .ensure_success()?; + Command::new("git") .current_dir(module.translation_repo) - .args(["pull", "origin", "main"]))?; + .args(["pull", "origin", "main"]) + .ensure_success()?; Ok(()) } fn push_new_templates(module: &Module) -> Result<()> { - run(Command::new("rsync") + Command::new("rsync") .args(["-ai", "--delete", "--no-perms", "--no-times", "-c"]) .args([ format!("{}/", module.template_folder), format!("{}/", module.translation_repo.join("templates")), - ]))?; + ]) + .ensure_success()?; let changes_pending = !Command::new("git") .current_dir(module.translation_repo) .args(["diff", "--exit-code"]) .status() - .whatever_context("git")? + .context("git")? .success(); if changes_pending { commit(module.translation_repo, "Update templates")?; @@ -100,11 +88,15 @@ fn push_new_templates(module: &Module) -> Result<()> { } fn push(repo: &Utf8Path) -> Result<()> { - run(Command::new("git") + Command::new("git") .current_dir(repo) - .args(["push", GIT_REMOTE, "main"]))?; + .args(["push", GIT_REMOTE, "main"]) + .ensure_success()?; // ensure origin matches ssh remote - run(Command::new("git").current_dir(repo).args(["fetch"]))?; + Command::new("git") + .current_dir(repo) + .args(["fetch"]) + .ensure_success()?; Ok(()) } @@ -112,8 +104,9 @@ fn commit(folder: F, message: &str) -> Result<()> where F: AsRef, { - run(Command::new("git") + Command::new("git") .current_dir(folder.as_ref()) - .args(["commit", "-a", "-m", message]))?; + .args(["commit", "-a", "-m", message]) + .ensure_success()?; Ok(()) } diff --git a/rslib/i18n_helpers/Cargo.toml b/rslib/i18n_helpers/Cargo.toml deleted file mode 100644 index b7d51cd4d..000000000 --- a/rslib/i18n_helpers/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "anki_i18n_helpers" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -publish = false -rust-version.workspace = true -description = "Helpers for Anki's i18n system" - -[lib] -name = "anki_i18n_helpers" -path = "src/lib.rs" - -[dependencies] -fluent-syntax.workspace = true -lazy_static.workspace = true -regex.workspace = true -serde_json.workspace = true -walkdir.workspace = true diff --git a/rslib/i18n_helpers/src/bin/deprecate_ftl_entries.rs b/rslib/i18n_helpers/src/bin/deprecate_ftl_entries.rs deleted file mode 100644 index 3b4081a27..000000000 --- a/rslib/i18n_helpers/src/bin/deprecate_ftl_entries.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -/// Deprecate unused ftl entries by moving them to the bottom of the file and -/// adding a deprecation warning. An entry is considered unused if cannot be -/// found in a source or JSON file. -/// Arguments before `--` are roots of ftl files, arguments after that are -/// source roots. JSON roots must be preceded by `--keep` or `-k`. -fn main() { - let args = Arguments::new(); - anki_i18n_helpers::garbage_collection::deprecate_ftl_entries( - &args.ftl_roots, - &args.source_roots, - &args.json_roots, - ); -} - -#[derive(Default)] -struct Arguments { - ftl_roots: Vec, - source_roots: Vec, - json_roots: Vec, -} - -impl Arguments { - fn new() -> Self { - let mut args = Self::default(); - let mut past_separator = false; - let mut keep_flag = false; - for arg in std::env::args() { - match arg.as_str() { - "--" => { - past_separator = true; - } - "--keep" | "-k" => { - keep_flag = true; - } - _ if keep_flag => { - keep_flag = false; - args.json_roots.push(arg) - } - _ if past_separator => args.source_roots.push(arg), - _ => args.ftl_roots.push(arg), - }; - } - - args - } -} diff --git a/rslib/i18n_helpers/src/bin/garbage_collect_ftl_entries.rs b/rslib/i18n_helpers/src/bin/garbage_collect_ftl_entries.rs deleted file mode 100644 index bfdf21588..000000000 --- a/rslib/i18n_helpers/src/bin/garbage_collect_ftl_entries.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -/// Delete every entry in the ftl files that is not mentioned in another message -/// or a given json. -/// First argument is the root of the json files, following are the roots of the -/// ftl files. -fn main() { - let args: Vec = std::env::args().collect(); - anki_i18n_helpers::garbage_collection::garbage_collect_ftl_entries(&args[2..], &args[1]); -} diff --git a/rslib/i18n_helpers/src/bin/write_ftl_json.rs b/rslib/i18n_helpers/src/bin/write_ftl_json.rs deleted file mode 100644 index 1a88abab7..000000000 --- a/rslib/i18n_helpers/src/bin/write_ftl_json.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -/// Extract references from all Rust, Python, TS, Svelte and Designer files in -/// the given roots, convert them to ftl names case and write them as a json to -/// the target file. -/// First argument is the target file name, following are source roots. -fn main() { - let args: Vec = std::env::args().collect(); - anki_i18n_helpers::garbage_collection::write_ftl_json(&args[2..], &args[1]); -} diff --git a/rslib/i18n_helpers/src/lib.rs b/rslib/i18n_helpers/src/lib.rs deleted file mode 100644 index 74f30485e..000000000 --- a/rslib/i18n_helpers/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -pub mod garbage_collection; -pub mod serialize;