Skip to content

Commit

Permalink
feat(cli): Add --frozen flag to error out if lockfile is out of date (
Browse files Browse the repository at this point in the history
#24355)

Closes #18296.

Adds a `--frozen` (alias `--frozen-lockfile`) flag that errors out if
the lockfile is out of date. This is useful for running in CI (where an
out of date lockfile is usually a mistake) or to prevent accidental
changes in dependencies.

![Screenshot 2024-06-26 at 7 11
13 PM](https://github.com/denoland/deno/assets/17734409/538404b8-b422-4f05-89e8-4c9b1c248576)
  • Loading branch information
nathanwhit committed Jul 2, 2024
1 parent d379c0b commit c13b6d1
Show file tree
Hide file tree
Showing 29 changed files with 477 additions and 30 deletions.
65 changes: 59 additions & 6 deletions cli/args/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 498,7 @@ pub struct Flags {
pub argv: Vec<String>,
pub subcommand: DenoSubcommand,

pub frozen_lockfile: bool,
pub ca_stores: Option<Vec<String>>,
pub ca_data: Option<CaData>,
pub cache_blocklist: Vec<String>,
Expand Down Expand Up @@ -1487,12 1488,15 @@ Future runs of this module will trigger no downloads or compilation unless
--reload is specified.",
)
.defer(|cmd| {
compile_args(cmd).arg(check_arg(false)).arg(
Arg::new("file")
.num_args(1..)
.required(true)
.value_hint(ValueHint::FilePath),
)
compile_args(cmd)
.arg(check_arg(false))
.arg(
Arg::new("file")
.num_args(1..)
.required(true)
.value_hint(ValueHint::FilePath),
)
.arg(frozen_lockfile_arg())
})
}

Expand Down Expand Up @@ -3271,6 3275,7 @@ fn runtime_args(
app
};
app
.arg(frozen_lockfile_arg())
.arg(cached_only_arg())
.arg(location_arg())
.arg(v8_flags_arg())
Expand Down Expand Up @@ -3384,6 3389,17 @@ fn cached_only_arg() -> Arg {
.help("Require that remote dependencies are already cached")
}

fn frozen_lockfile_arg() -> Arg {
Arg::new("frozen")
.long("frozen")
.alias("frozen-lockfile")
.value_parser(value_parser!(bool))
.num_args(0..=1)
.require_equals(true)
.default_missing_value("true")
.help("Error out if lockfile is out of date")
}

/// Used for subcommands that operate on executable scripts only.
/// `deno fmt` has its own `--ext` arg because its possible values differ.
/// If --ext is not provided and the script doesn't have a file extension,
Expand Down Expand Up @@ -3774,6 3790,7 @@ fn bundle_parse(flags: &mut Flags, matches: &mut ArgMatches) {

fn cache_parse(flags: &mut Flags, matches: &mut ArgMatches) {
compile_args_parse(flags, matches);
frozen_lockfile_arg_parse(flags, matches);
let files = matches.remove_many::<String>("file").unwrap().collect();
flags.subcommand = DenoSubcommand::Cache(CacheFlags { files });
}
Expand Down Expand Up @@ -4576,6 4593,7 @@ fn runtime_args_parse(
) {
compile_args_parse(flags, matches);
cached_only_arg_parse(flags, matches);
frozen_lockfile_arg_parse(flags, matches);
if include_perms {
permission_args_parse(flags, matches);
}
Expand Down Expand Up @@ -4667,6 4685,12 @@ fn cached_only_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) {
}
}

fn frozen_lockfile_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) {
if let Some(&v) = matches.get_one::<bool>("frozen") {
flags.frozen_lockfile = v;
}
}

fn ext_arg_parse(flags: &mut Flags, matches: &mut ArgMatches) {
flags.ext = matches.remove_one::<String>("ext");
}
Expand Down Expand Up @@ -9845,4 9869,33 @@ mod tests {
}
);
}

#[test]
fn run_with_frozen_lockfile() {
let cases = [
(Some("--frozen"), true),
(Some("--frozen=true"), true),
(Some("--frozen=false"), false),
(None, false),
];
for (flag, frozen) in cases {
let mut args = svec!["deno", "run"];
if let Some(f) = flag {
args.push(f.into());
}
args.push("script.ts".into());
let r = flags_from_vec(args);
assert_eq!(
r.unwrap(),
Flags {
subcommand: DenoSubcommand::Run(RunFlags::new_default(
"script.ts".to_string(),
)),
frozen_lockfile: frozen,
code_cache_enabled: true,
..Flags::default()
}
);
}
}
}
55 changes: 45 additions & 10 deletions cli/args/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 23,7 @@ use deno_lockfile::Lockfile;
pub struct CliLockfile {
lockfile: Mutex<Lockfile>,
pub filename: PathBuf,
pub frozen: bool,
}

pub struct Guard<'a, T> {
Expand All @@ -44,11 45,12 @@ impl<'a, T> std::ops::DerefMut for Guard<'a, T> {
}

impl CliLockfile {
pub fn new(lockfile: Lockfile) -> Self {
pub fn new(lockfile: Lockfile, frozen: bool) -> Self {
let filename = lockfile.filename.clone();
Self {
lockfile: Mutex::new(lockfile),
filename,
frozen,
}
}

Expand All @@ -71,6 73,7 @@ impl CliLockfile {
}

pub fn write_if_changed(&self) -> Result<(), AnyError> {
self.error_if_changed()?;
let mut lockfile = self.lockfile.lock();
let Some(bytes) = lockfile.resolve_write_bytes() else {
return Ok(()); // nothing to do
Expand Down Expand Up @@ -127,23 130,55 @@ impl CliLockfile {
};

let lockfile = if flags.lock_write {
CliLockfile::new(Lockfile::new_empty(filename, true))
CliLockfile::new(
Lockfile::new_empty(filename, true),
flags.frozen_lockfile,
)
} else {
Self::read_from_path(filename)?
Self::read_from_path(filename, flags.frozen_lockfile)?
};
Ok(Some(lockfile))
}
pub fn read_from_path(filename: PathBuf) -> Result<CliLockfile, AnyError> {
pub fn read_from_path(
filename: PathBuf,
frozen: bool,
) -> Result<CliLockfile, AnyError> {
match std::fs::read_to_string(&filename) {
Ok(text) => Ok(CliLockfile::new(Lockfile::with_lockfile_content(
filename, &text, false,
)?)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
Ok(CliLockfile::new(Lockfile::new_empty(filename, false)))
}
Ok(text) => Ok(CliLockfile::new(
Lockfile::with_lockfile_content(filename, &text, false)?,
frozen,
)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(
CliLockfile::new(Lockfile::new_empty(filename, false), frozen),
),
Err(err) => Err(err).with_context(|| {
format!("Failed reading lockfile '{}'", filename.display())
}),
}
}
pub fn error_if_changed(&self) -> Result<(), AnyError> {
if !self.frozen {
return Ok(());
}
let lockfile = self.lockfile.lock();
if lockfile.has_content_changed {
let suggested = if *super::DENO_FUTURE {
"`deno cache --frozen=false`, `deno install --frozen=false`,"
} else {
"`deno cache --frozen=false`"
};

let contents =
std::fs::read_to_string(&lockfile.filename).unwrap_or_default();
let new_contents = lockfile.as_json_string();
let diff = crate::util::diff::diff(&contents, &new_contents);
// has an extra newline at the end
let diff = diff.trim_end();
Err(deno_core::anyhow::anyhow!(
"The lockfile is out of date. Run {suggested} or rerun with `--frozen=false` to update it.\nchanges:\n{diff}"
))
} else {
Ok(())
}
}
}
2 changes: 1 addition & 1 deletion cli/lsp/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1825,7 1825,7 @@ fn resolve_node_modules_dir(
}

fn resolve_lockfile_from_path(lockfile_path: PathBuf) -> Option<CliLockfile> {
match CliLockfile::read_from_path(lockfile_path) {
match CliLockfile::read_from_path(lockfile_path, false) {
Ok(value) => {
if value.filename.exists() {
if let Ok(specifier) = ModuleSpecifier::from_file_path(&value.filename)
Expand Down
11 changes: 7 additions & 4 deletions cli/module_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 72,13 @@ use deno_semver::npm::NpmPackageReqReference;
pub async fn load_top_level_deps(factory: &CliFactory) -> Result<(), AnyError> {
let npm_resolver = factory.npm_resolver().await?;
if let Some(npm_resolver) = npm_resolver.as_managed() {
npm_resolver.ensure_top_level_package_json_install().await?;
// TODO(nathanwhit): we call `cache_packages` if the lockfile is modified,
// so by calling it here it's possible we end up calling it twice
npm_resolver.cache_packages().await?;
if !npm_resolver.ensure_top_level_package_json_install().await? {
if let Some(lockfile) = factory.maybe_lockfile() {
lockfile.error_if_changed()?;
}

npm_resolver.cache_packages().await?;
}
}
// cache as many entries in the import map as we can
if let Some(import_map) = factory.maybe_import_map().await? {
Expand Down
28 changes: 21 additions & 7 deletions cli/npm/managed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,12 361,15 @@ impl ManagedCliNpmResolver {
}

/// Adds package requirements to the resolver and ensures everything is setup.
/// This includes setting up the `node_modules` directory, if applicable.
pub async fn add_package_reqs(
&self,
packages: &[PackageReq],
) -> Result<(), AnyError> {
let result = self.add_package_reqs_raw(packages).await;
result.dependencies_result
self
.add_package_reqs_raw(packages)
.await
.dependencies_result
}

pub async fn add_package_reqs_raw(
Expand All @@ -381,6 384,12 @@ impl ManagedCliNpmResolver {
}

let mut result = self.resolution.add_package_reqs(packages).await;

if result.dependencies_result.is_ok() {
if let Some(lockfile) = self.maybe_lockfile.as_ref() {
result.dependencies_result = lockfile.error_if_changed();
}
}
if result.dependencies_result.is_ok() {
result.dependencies_result =
self.cache_packages().await.map_err(AnyError::from);
Expand Down Expand Up @@ -442,14 451,19 @@ impl ManagedCliNpmResolver {
self.resolution.resolve_pkg_id_from_pkg_req(req)
}

/// Ensures that the top level `package.json` dependencies are installed.
/// This may set up the `node_modules` directory.
///
/// Returns `true` if any changes (such as caching packages) were made.
/// If this returns `false`, `node_modules` has _not_ been set up.
pub async fn ensure_top_level_package_json_install(
&self,
) -> Result<(), AnyError> {
) -> Result<bool, AnyError> {
let Some(reqs) = self.package_json_deps_provider.reqs() else {
return Ok(());
return Ok(false);
};
if !self.top_level_install_flag.raise() {
return Ok(()); // already did this
return Ok(false); // already did this
}
// check if something needs resolving before bothering to load all
// the package information (which is slow)
Expand All @@ -460,11 474,11 @@ impl ManagedCliNpmResolver {
log::debug!(
"All package.json deps resolvable. Skipping top level install."
);
return Ok(()); // everything is already resolvable
return Ok(false); // everything is already resolvable
}

let reqs = reqs.into_iter().cloned().collect::<Vec<_>>();
self.add_package_reqs(&reqs).await
self.add_package_reqs(&reqs).await.map(|_| true)
}

pub async fn cache_package_info(
Expand Down
5 changes: 4 additions & 1 deletion cli/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 824,10 @@ impl<'a> deno_graph::source::NpmResolver for WorkerCliNpmGraphResolver<'a> {
};

let top_level_result = if self.found_package_json_dep_flag.is_raised() {
npm_resolver.ensure_top_level_package_json_install().await
npm_resolver
.ensure_top_level_package_json_install()
.await
.map(|_| ())
} else {
Ok(())
};
Expand Down
4 changes: 4 additions & 0 deletions cli/tools/installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 466,10 @@ async fn resolve_shim_data(
executable_args.push("--cached-only".to_string());
}

if flags.frozen_lockfile {
executable_args.push("--frozen".to_string());
}

if resolve_no_prompt(&flags.permissions) {
executable_args.push("--no-prompt".to_string());
}
Expand Down
Loading

0 comments on commit c13b6d1

Please sign in to comment.