tidy: use a lockfile for js tools instead of npx

this makes us less vulnerable to MITM and supply chain attacks.

it also means that the CI scripts are no longer responsible for
tracking the versions of these tools.

it should also avoid the situation where local tsc and CI
disagree on the presense of errors due to them being different versions.
This commit is contained in:
binarycat
2025-06-27 14:29:39 -05:00
parent 4bd3b74aa9
commit c8e2a65ed1
7 changed files with 3323 additions and 86 deletions

2
.gitignore vendored
View File

@@ -85,8 +85,6 @@ __pycache__/
## Node
node_modules
package-lock.json
package.json
/src/doc/rustc-dev-guide/mermaid.min.js
## Rustdoc GUI tests

3232
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

9
package.json Normal file
View File

@@ -0,0 +1,9 @@
{
"dependencies": {
"browser-ui-test": "^0.21.1",
"es-check": "^6.2.1",
"eslint": "^8.57.1",
"eslint-js": "github:eslint/js",
"typescript": "^5.8.3"
}
}

View File

@@ -27,10 +27,6 @@ COPY scripts/nodejs.sh /scripts/
RUN sh /scripts/nodejs.sh /node
ENV PATH="/node/bin:${PATH}"
# Install es-check
# Pin its version to prevent unrelated CI failures due to future es-check versions.
RUN npm install es-check@6.1.1 eslint@8.6.0 typescript@5.7.3 -g
COPY scripts/sccache.sh /scripts/
RUN sh /scripts/sccache.sh

View File

@@ -50,7 +50,6 @@ pub fn check(
ci_info: &CiInfo,
librustdoc_path: &Path,
tools_path: &Path,
src_path: &Path,
bless: bool,
extra_checks: Option<&str>,
pos_args: &[String],
@@ -62,7 +61,6 @@ pub fn check(
ci_info,
librustdoc_path,
tools_path,
src_path,
bless,
extra_checks,
pos_args,
@@ -77,7 +75,6 @@ fn check_impl(
ci_info: &CiInfo,
librustdoc_path: &Path,
tools_path: &Path,
src_path: &Path,
bless: bool,
extra_checks: Option<&str>,
pos_args: &[String],
@@ -295,13 +292,17 @@ fn check_impl(
spellcheck_runner(&args)?;
}
if js_lint || js_typecheck {
rustdoc_js::npm_install(root_path, outdir)?;
}
if js_lint {
rustdoc_js::lint(librustdoc_path, tools_path, src_path)?;
rustdoc_js::es_check(librustdoc_path)?;
rustdoc_js::lint(outdir, librustdoc_path, tools_path)?;
rustdoc_js::es_check(outdir, librustdoc_path)?;
}
if js_typecheck {
rustdoc_js::typecheck(librustdoc_path)?;
rustdoc_js::typecheck(outdir, librustdoc_path)?;
}
Ok(())

View File

@@ -3,12 +3,61 @@
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Child, Command};
use std::{fs, io};
use build_helper::ci::CiEnv;
use ignore::DirEntry;
use crate::walk::walk_no_read;
fn node_module_bin(outdir: &Path, name: &str) -> PathBuf {
outdir.join("node_modules/.bin").join(name)
}
fn spawn_cmd(cmd: &mut Command) -> Result<Child, io::Error> {
cmd.spawn().map_err(|err| {
eprintln!("unable to run {cmd:?} due to {err:?}");
err
})
}
/// install all js dependencies from package.json.
pub(super) fn npm_install(root_path: &Path, outdir: &Path) -> Result<(), super::Error> {
let copy_to_build = |p| {
fs::copy(root_path.join(p), outdir.join(p)).map_err(|e| {
eprintln!("unable to copy {p:?} to build directory: {e:?}");
e
})
};
// copy stuff to the output directory to make node_modules get put there.
copy_to_build("package.json")?;
copy_to_build("package-lock.json")?;
let mut cmd = Command::new("npm");
if CiEnv::is_ci() {
// `npm ci` redownloads every time and thus is too slow for local development.
cmd.arg("ci");
} else {
cmd.arg("install");
}
// disable a bunch of things we don't want.
// this makes tidy output less noisy, and also significantly improves runtime
// of repeated tidy invokations.
cmd.args(&["--audit=false", "--save=false", "--fund=false"]);
cmd.current_dir(outdir);
let mut child = spawn_cmd(&mut cmd)?;
match child.wait() {
Ok(exit_status) => {
if exit_status.success() {
return Ok(());
}
Err(super::Error::FailedCheck("npm install"))
}
Err(error) => Err(super::Error::Generic(format!("npm install failed: {error:?}"))),
}
}
fn rustdoc_js_files(librustdoc_path: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
walk_no_read(
@@ -21,13 +70,13 @@ fn rustdoc_js_files(librustdoc_path: &Path) -> Vec<PathBuf> {
return files;
}
fn run_eslint(args: &[PathBuf], config_folder: PathBuf) -> Result<(), super::Error> {
let mut child = Command::new("npx")
.arg("eslint")
.arg("-c")
.arg(config_folder.join(".eslintrc.js"))
.args(args)
.spawn()?;
fn run_eslint(outdir: &Path, args: &[PathBuf], config_folder: PathBuf) -> Result<(), super::Error> {
let mut child = spawn_cmd(
Command::new(node_module_bin(outdir, "eslint"))
.arg("-c")
.arg(config_folder.join(".eslintrc.js"))
.args(args),
)?;
match child.wait() {
Ok(exit_status) => {
if exit_status.success() {
@@ -39,77 +88,31 @@ fn run_eslint(args: &[PathBuf], config_folder: PathBuf) -> Result<(), super::Err
}
}
fn get_eslint_version_inner(global: bool) -> Option<String> {
let mut command = Command::new("npm");
command.arg("list").arg("--parseable").arg("--long").arg("--depth=0");
if global {
command.arg("--global");
}
let output = command.output().ok()?;
let lines = String::from_utf8_lossy(&output.stdout);
lines.lines().find_map(|l| l.split(':').nth(1)?.strip_prefix("eslint@")).map(|v| v.to_owned())
}
fn get_eslint_version() -> Option<String> {
get_eslint_version_inner(false).or_else(|| get_eslint_version_inner(true))
}
pub(super) fn lint(
outdir: &Path,
librustdoc_path: &Path,
tools_path: &Path,
src_path: &Path,
) -> Result<(), super::Error> {
let eslint_version_path = src_path.join("ci/docker/host-x86_64/tidy/eslint.version");
let eslint_version = match std::fs::read_to_string(&eslint_version_path) {
Ok(version) => version.trim().to_string(),
Err(error) => {
eprintln!("failed to read `{}`: {error:?}", eslint_version_path.display());
return Err(error.into());
}
};
// Having the correct `eslint` version installed via `npm` isn't strictly necessary, since we're invoking it via `npx`,
// but this check allows the vast majority that is not working on the rustdoc frontend to avoid the penalty of running
// `eslint` in tidy. See also: https://github.com/rust-lang/rust/pull/142851
match get_eslint_version() {
Some(version) => {
if version != eslint_version {
// unfortunatly we can't use `Error::Version` here becuse `str::trim` isn't const and
// Version::required must be a static str
return Err(super::Error::Generic(format!(
"⚠️ Installed version of eslint (`{version}`) is different than the \
one used in the CI (`{eslint_version}`)\n\
You can install this version using `npm update eslint` or by using \
`npm install eslint@{eslint_version}`\n
"
)));
}
}
None => {
//eprintln!("`eslint` doesn't seem to be installed. Skipping tidy check for JS files.");
//eprintln!("You can install it using `npm install eslint@{eslint_version}`");
return Err(super::Error::MissingReq(
"eslint",
"js lint checks",
Some(format!("You can install it using `npm install eslint@{eslint_version}`")),
));
}
}
let files_to_check = rustdoc_js_files(librustdoc_path);
println!("Running eslint on rustdoc JS files");
run_eslint(&files_to_check, librustdoc_path.join("html/static"))?;
run_eslint(outdir, &files_to_check, librustdoc_path.join("html/static"))?;
run_eslint(&[tools_path.join("rustdoc-js/tester.js")], tools_path.join("rustdoc-js"))?;
run_eslint(&[tools_path.join("rustdoc-gui/tester.js")], tools_path.join("rustdoc-gui"))?;
run_eslint(outdir, &[tools_path.join("rustdoc-js/tester.js")], tools_path.join("rustdoc-js"))?;
run_eslint(
outdir,
&[tools_path.join("rustdoc-gui/tester.js")],
tools_path.join("rustdoc-gui"),
)?;
Ok(())
}
pub(super) fn typecheck(librustdoc_path: &Path) -> Result<(), super::Error> {
pub(super) fn typecheck(outdir: &Path, librustdoc_path: &Path) -> Result<(), super::Error> {
// use npx to ensure correct version
let mut child = Command::new("npx")
.arg("tsc")
.arg("-p")
.arg(librustdoc_path.join("html/static/js/tsconfig.json"))
.spawn()?;
let mut child = spawn_cmd(
Command::new(node_module_bin(outdir, "tsc"))
.arg("-p")
.arg(librustdoc_path.join("html/static/js/tsconfig.json")),
)?;
match child.wait() {
Ok(exit_status) => {
if exit_status.success() {
@@ -121,15 +124,14 @@ pub(super) fn typecheck(librustdoc_path: &Path) -> Result<(), super::Error> {
}
}
pub(super) fn es_check(librustdoc_path: &Path) -> Result<(), super::Error> {
pub(super) fn es_check(outdir: &Path, librustdoc_path: &Path) -> Result<(), super::Error> {
let files_to_check = rustdoc_js_files(librustdoc_path);
// use npx to ensure correct version
let mut cmd = Command::new("npx");
cmd.arg("es-check").arg("es2019");
let mut cmd = Command::new(node_module_bin(outdir, "es-check"));
cmd.arg("es2019");
for f in files_to_check {
cmd.arg(f);
}
match cmd.spawn()?.wait() {
match spawn_cmd(&mut cmd)?.wait() {
Ok(exit_status) => {
if exit_status.success() {
return Ok(());

View File

@@ -182,7 +182,6 @@ fn main() {
&ci_info,
&librustdoc_path,
&tools_path,
&src_path,
bless,
extra_checks,
pos_args