Skip to content

Commit e2c8f3c

Browse files
authored
feat: Add a typos CI job (#16122)
Over the past few months, I have noticed various PRs that were created to fix typos, and figured that it would be a good idea to add a non-blocking[^1] CI job that runs [`typos`](https://github.com/crate-ci/typos)[^2] to hopefully catch typos before they are merged into the codebase. The configuration file for `typos` in this PR is meant to get the ball rolling and is by no means perfect. [^1]: I chose to make this non-blocking, so any newly added corrections don't cause CI to fail. [^2]: The workflow file for the job matches the one in [`annotate-snippets-rs`](https://github.com/rust-lang/annotate-snippets-rs/blob/5a632cdfadb5902bf063722f80b37fcb50da0416/.github/workflows/spelling.yml)
2 parents 2d4fa13 + 6581a5d commit e2c8f3c

File tree

34 files changed

+375
-86
lines changed

34 files changed

+375
-86
lines changed

.cargo/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ build-man = "run --package xtask-build-man --"
33
stale-label = "run --package xtask-stale-label --"
44
bump-check = "run --package xtask-bump-check --"
55
lint-docs = "run --package xtask-lint-docs --"
6+
spellcheck = "run --package xtask-spellcheck --"
67

78
[env]
89
# HACK: Until this is stabilized, `snapbox`s polyfill could get confused

.github/workflows/main.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
- resolver
2929
- rustfmt
3030
- schema
31+
- spellcheck
3132
- test
3233
- test_gitoxide
3334
permissions:
@@ -304,3 +305,12 @@ jobs:
304305
- uses: actions/checkout@v5
305306
- uses: taiki-e/install-action@cargo-hack
306307
- run: cargo hack check --all-targets --rust-version --workspace --ignore-private --locked
308+
309+
spellcheck:
310+
name: Spell Check with Typos
311+
runs-on: ubuntu-latest
312+
steps:
313+
- name: Checkout Actions Repository
314+
uses: actions/checkout@v5
315+
- name: Spell Check Repo
316+
uses: crate-ci/[email protected]

Cargo.lock

Lines changed: 13 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ cargo-test-macro = { version = "0.4.8", path = "crates/cargo-test-macro" }
3535
cargo-test-support = { version = "0.9.1", path = "crates/cargo-test-support" }
3636
cargo-util = { version = "0.2.26", path = "crates/cargo-util" }
3737
cargo-util-schemas = { version = "0.10.3", path = "crates/cargo-util-schemas" }
38-
cargo_metadata = "0.23.0"
38+
cargo_metadata = "0.23.1"
3939
clap = "4.5.51"
4040
clap_complete = { version = "4.5.60", features = ["unstable-dynamic"] }
4141
color-print = "0.3.7"

crates/cargo-test-support/src/paths.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ fn build_dir_ignored_path_patterns() -> Vec<String> {
357357
// Ignore MacOS debug symbols as there are many files/directories that would clutter up
358358
// tests few not a lot of benefit.
359359
"[..].dSYM/[..]",
360-
// Ignore Windows debub symbols files (.pdb)
360+
// Ignore Windows debug symbols files (.pdb)
361361
"[..].pdb",
362362
]
363363
.into_iter()

crates/cargo-util-schemas/src/lockfile.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ impl TomlLockfileSourceId {
110110
.ok_or_else(|| TomlLockfileSourceIdErrorKind::InvalidSource(source.clone()))?;
111111

112112
// Sparse URLs store the kind prefix (sparse+) in the URL. Therefore, for sparse kinds, we
113-
// want to use the raw `source` instead of the splitted `url`.
113+
// want to use the raw `source` instead of the split `url`.
114114
let url = Url::parse(if kind == "sparse" { &source } else { url }).map_err(|msg| {
115115
TomlLockfileSourceIdErrorKind::InvalidUrl {
116116
url: url.to_string(),

crates/xtask-spellcheck/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "xtask-spellcheck"
3+
version = "0.0.0"
4+
edition.workspace = true
5+
publish = false
6+
7+
[dependencies]
8+
anyhow.workspace = true
9+
cargo_metadata.workspace = true
10+
cargo-util.workspace = true
11+
clap.workspace = true
12+
semver.workspace = true
13+
14+
[lints]
15+
workspace = true
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#![allow(clippy::disallowed_methods)]
2+
#![allow(clippy::print_stderr)]
3+
#![allow(clippy::print_stdout)]
4+
5+
use anyhow::Result;
6+
use cargo_metadata::{Metadata, MetadataCommand};
7+
use clap::{Arg, ArgAction};
8+
use semver::Version;
9+
use std::{
10+
env, io,
11+
path::{Path, PathBuf},
12+
process::Command,
13+
};
14+
15+
const BIN_NAME: &str = "typos";
16+
const PKG_NAME: &str = "typos-cli";
17+
const TYPOS_STEP_PREFIX: &str = " uses: crate-ci/typos@v";
18+
19+
fn main() -> anyhow::Result<()> {
20+
let cli = cli();
21+
exec(&cli.get_matches())?;
22+
Ok(())
23+
}
24+
25+
pub fn cli() -> clap::Command {
26+
clap::Command::new("xtask-spellcheck")
27+
.arg(
28+
Arg::new("color")
29+
.long("color")
30+
.help("Coloring: auto, always, never")
31+
.action(ArgAction::Set)
32+
.value_name("WHEN")
33+
.global(true),
34+
)
35+
.arg(
36+
Arg::new("quiet")
37+
.long("quiet")
38+
.short('q')
39+
.help("Do not print cargo log messages")
40+
.action(ArgAction::SetTrue)
41+
.global(true),
42+
)
43+
.arg(
44+
Arg::new("verbose")
45+
.long("verbose")
46+
.short('v')
47+
.help("Use verbose output (-vv very verbose/build.rs output)")
48+
.action(ArgAction::Count)
49+
.global(true),
50+
)
51+
.arg(
52+
Arg::new("write-changes")
53+
.long("write-changes")
54+
.short('w')
55+
.help("Write fixes out")
56+
.action(ArgAction::SetTrue)
57+
.global(true),
58+
)
59+
}
60+
61+
pub fn exec(matches: &clap::ArgMatches) -> Result<()> {
62+
let mut args = vec![];
63+
64+
match matches.get_one::<String>("color") {
65+
Some(c) if matches!(c.as_str(), "auto" | "always" | "never") => {
66+
args.push("--color");
67+
args.push(c);
68+
}
69+
Some(c) => {
70+
anyhow::bail!(
71+
"argument for --color must be auto, always, or \
72+
never, but found `{}`",
73+
c
74+
);
75+
}
76+
_ => {}
77+
}
78+
79+
if matches.get_flag("quiet") {
80+
args.push("--quiet");
81+
}
82+
83+
let verbose_count = matches.get_count("verbose");
84+
85+
for _ in 0..verbose_count {
86+
args.push("--verbose");
87+
}
88+
if matches.get_flag("write-changes") {
89+
args.push("--write-changes");
90+
}
91+
92+
let metadata = MetadataCommand::new()
93+
.exec()
94+
.expect("cargo_metadata failed");
95+
96+
let required_version = extract_workflow_typos_version(&metadata)?;
97+
98+
let outdir = metadata
99+
.build_directory
100+
.unwrap_or_else(|| metadata.target_directory)
101+
.as_std_path()
102+
.join("tmp");
103+
let workspace_root = metadata.workspace_root.as_path().as_std_path();
104+
let bin_path = crate::ensure_version_or_cargo_install(&outdir, required_version)?;
105+
106+
eprintln!("running {BIN_NAME}");
107+
Command::new(bin_path)
108+
.current_dir(workspace_root)
109+
.args(args)
110+
.status()?;
111+
112+
Ok(())
113+
}
114+
115+
fn extract_workflow_typos_version(metadata: &Metadata) -> anyhow::Result<Version> {
116+
let ws_root = metadata.workspace_root.as_path().as_std_path();
117+
let workflow_path = ws_root.join(".github").join("workflows").join("main.yml");
118+
let file_content = std::fs::read_to_string(workflow_path)?;
119+
120+
if let Some(line) = file_content
121+
.lines()
122+
.find(|line| line.contains(TYPOS_STEP_PREFIX))
123+
&& let Some(stripped) = line.strip_prefix(TYPOS_STEP_PREFIX)
124+
&& let Ok(v) = Version::parse(stripped)
125+
{
126+
Ok(v)
127+
} else {
128+
Err(anyhow::anyhow!("Could not find typos version in workflow"))
129+
}
130+
}
131+
132+
/// If the given executable is installed with the given version, use that,
133+
/// otherwise install via cargo.
134+
pub fn ensure_version_or_cargo_install(
135+
build_dir: &Path,
136+
required_version: Version,
137+
) -> io::Result<PathBuf> {
138+
// Check if the user has a sufficient version already installed
139+
let bin_path = PathBuf::from(BIN_NAME).with_extension(env::consts::EXE_EXTENSION);
140+
if let Some(user_version) = get_typos_version(&bin_path) {
141+
if user_version >= required_version {
142+
return Ok(bin_path);
143+
}
144+
}
145+
146+
let tool_root_dir = build_dir.join("misc-tools");
147+
let tool_bin_dir = tool_root_dir.join("bin");
148+
let bin_path = tool_bin_dir
149+
.join(BIN_NAME)
150+
.with_extension(env::consts::EXE_EXTENSION);
151+
152+
// Check if we have already installed sufficient version
153+
if let Some(misc_tools_version) = get_typos_version(&bin_path) {
154+
if misc_tools_version >= required_version {
155+
return Ok(bin_path);
156+
}
157+
}
158+
159+
eprintln!("required `typos` version ({required_version}) not found, building from source");
160+
161+
let mut cmd = Command::new("cargo");
162+
// use --force to ensure that if the required version is bumped, we update it.
163+
cmd.args(["install", "--locked", "--force", "--quiet"])
164+
.arg("--root")
165+
.arg(&tool_root_dir)
166+
// use --target-dir to ensure we have a build cache so repeated invocations aren't slow.
167+
.arg("--target-dir")
168+
.arg(tool_root_dir.join("target"))
169+
.arg(format!("{PKG_NAME}@{required_version}"))
170+
// modify PATH so that cargo doesn't print a warning telling the user to modify the path.
171+
.env(
172+
"PATH",
173+
env::join_paths(
174+
env::split_paths(&env::var("PATH").unwrap())
175+
.chain(std::iter::once(tool_bin_dir.clone())),
176+
)
177+
.expect("build dir contains invalid char"),
178+
);
179+
180+
let cargo_exit_code = cmd.spawn()?.wait()?;
181+
if !cargo_exit_code.success() {
182+
return Err(io::Error::other("cargo install failed"));
183+
}
184+
assert!(
185+
matches!(bin_path.try_exists(), Ok(true)),
186+
"cargo install did not produce the expected binary"
187+
);
188+
eprintln!("finished {BIN_NAME}");
189+
Ok(bin_path)
190+
}
191+
192+
fn get_typos_version(bin: &PathBuf) -> Option<Version> {
193+
// ignore the process exit code here and instead just let the version number check fail
194+
if let Ok(output) = Command::new(&bin).arg("--version").output()
195+
&& let Ok(s) = String::from_utf8(output.stdout)
196+
&& let Some(version_str) = s.trim().split_whitespace().last()
197+
{
198+
Version::parse(version_str).ok()
199+
} else {
200+
None
201+
}
202+
}

src/cargo/core/compiler/build_runner/compilation_files.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,16 +235,16 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> {
235235
/// Note that some units may share the same directory, so care should be
236236
/// taken in those cases!
237237
fn pkg_dir(&self, unit: &Unit) -> String {
238-
let seperator = match self.ws.gctx().cli_unstable().build_dir_new_layout {
238+
let separator = match self.ws.gctx().cli_unstable().build_dir_new_layout {
239239
true => "/",
240240
false => "-",
241241
};
242242
let name = unit.pkg.package_id().name();
243243
let meta = self.metas[unit];
244244
if let Some(c_extra_filename) = meta.c_extra_filename() {
245-
format!("{}{}{}", name, seperator, c_extra_filename)
245+
format!("{}{}{}", name, separator, c_extra_filename)
246246
} else {
247-
format!("{}{}{}", name, seperator, self.target_short_hash(unit))
247+
format!("{}{}{}", name, separator, self.target_short_hash(unit))
248248
}
249249
}
250250

0 commit comments

Comments
 (0)