Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Handle package.json lifecycle scripts #23558

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 0 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Expand Up @@ -370,3 +370,7 @@ opt-level = 3
opt-level = 3
[profile.release.package.base64-simd]
opt-level = 3

[patch.crates-io]
deno_npm = { path = "../deno_npm" }
eszip = { path = "../eszip" }
84 changes: 55 additions & 29 deletions cli/args/flags.rs
Expand Up @@ -33,6 +33,7 @@ use std::str::FromStr;
use crate::util::fs::canonicalize_path;

use super::flags_net;
use super::DENO_FUTURE;

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct FileFlags {
Expand Down Expand Up @@ -824,8 +825,15 @@ impl Flags {
std::env::current_dir().ok()
}
Add(_) | Bundle(_) | Completions(_) | Doc(_) | Fmt(_) | Init(_)
| Install(_) | Uninstall(_) | Jupyter(_) | Lsp | Lint(_) | Types
| Uninstall(_) | Jupyter(_) | Lsp | Lint(_) | Types
| Upgrade(_) | Vendor(_) => None,
Install(_) => {
if *DENO_FUTURE {
std::env::current_dir().ok()
} else {
None
}
},
}
}

Expand Down Expand Up @@ -2002,25 +2010,36 @@ The installation root is determined, in order of precedence:
- $HOME/.deno

These must be added to the path manually if required.")
.defer(|cmd| runtime_args(cmd, true, true).arg(Arg::new("cmd").required(true).num_args(1..).value_hint(ValueHint::FilePath))
.arg(check_arg(true))
.arg(
.defer(|cmd| {
let cmd =
runtime_args(cmd, true, true)
.arg(check_arg(true));

let cmd = if *DENO_FUTURE {
cmd.arg(Arg::new("cmd").required_if_eq("global", "true").num_args(1..).value_hint(ValueHint::FilePath))
} else {
cmd.arg(Arg::new("cmd").required(true).num_args(1..).value_hint(ValueHint::FilePath))
};

cmd.arg(
Arg::new("name")
.long("name")
.short('n')
.help("Executable file name")
.required(false))
.required(false)
)
.arg(
Arg::new("root")
.long("root")
.help("Installation root")
.value_hint(ValueHint::DirPath))
.value_hint(ValueHint::DirPath)
)
.arg(
Arg::new("force")
.long("force")
.short('f')
.help("Forcefully overwrite existing installation")
.action(ArgAction::SetTrue))
.action(ArgAction::SetTrue)
)
.arg(
Arg::new("global")
Expand All @@ -2030,6 +2049,7 @@ These must be added to the path manually if required.")
.action(ArgAction::SetTrue)
)
.arg(env_file_arg())
})
}

fn jupyter_subcommand() -> Command {
Expand Down Expand Up @@ -3794,29 +3814,35 @@ fn info_parse(flags: &mut Flags, matches: &mut ArgMatches) {

fn install_parse(flags: &mut Flags, matches: &mut ArgMatches) {
runtime_args_parse(flags, matches, true, true);

let root = matches.remove_one::<String>("root");

let force = matches.get_flag("force");

let global = matches.get_flag("global");
let name = matches.remove_one::<String>("name");
let mut cmd_values = matches.remove_many::<String>("cmd").unwrap();

let module_url = cmd_values.next().unwrap();
let args = cmd_values.collect();

flags.subcommand = DenoSubcommand::Install(InstallFlags {
// TODO(bartlomieju): remove once `deno install` supports both local and
// global installs
global,
kind: InstallKind::Global(InstallFlagsGlobal {
name,
module_url,
args,
root,
force,
}),
});
if global {
let root = matches.remove_one::<String>("root");
let force = matches.get_flag("force");
let name = matches.remove_one::<String>("name");
let mut cmd_values = matches.remove_many::<String>("cmd").unwrap_or_default();

let module_url = cmd_values.next().unwrap();
let args = cmd_values.collect();

flags.subcommand = DenoSubcommand::Install(InstallFlags {
// TODO(bartlomieju): remove once `deno install` supports both local and
// global installs
global,
kind: InstallKind::Global(InstallFlagsGlobal {
name,
module_url,
args,
root,
force,
}),
});
} else {
flags.subcommand = DenoSubcommand::Install(InstallFlags {
global,
kind: InstallKind::Local,
})
}
}

fn jupyter_parse(flags: &mut Flags, matches: &mut ArgMatches) {
Expand Down
2 changes: 1 addition & 1 deletion cli/args/mod.rs
Expand Up @@ -720,7 +720,7 @@ pub struct CliOptions {
maybe_node_modules_folder: Option<PathBuf>,
maybe_vendor_folder: Option<PathBuf>,
maybe_config_file: Option<ConfigFile>,
maybe_package_json: Option<PackageJson>,
pub maybe_package_json: Option<PackageJson>,
maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
overrides: CliOptionOverrides,
maybe_workspace_config: Option<WorkspaceConfig>,
Expand Down
3 changes: 2 additions & 1 deletion cli/factory.rs
Expand Up @@ -408,7 +408,8 @@ impl CliFactory {
.npm_resolver
.get_or_try_init_async(async {
let fs = self.fs();
create_cli_npm_resolver(if self.options.use_byonm() {
// For `deno install` we want to force the managed resolver so it can set up `node_modules/` directory.
create_cli_npm_resolver(if self.options.use_byonm() && !matches!(self.options.sub_command(), DenoSubcommand::Install(_)) {
CliNpmResolverCreateOptions::Byonm(CliNpmResolverByonmCreateOptions {
fs: fs.clone(),
root_node_modules_dir: match self.options.node_modules_dir_path() {
Expand Down
122 changes: 122 additions & 0 deletions cli/npm/managed/resolvers/local.rs
Expand Up @@ -7,6 +7,8 @@ use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::io::Write;
use std::os::unix::fs::symlink;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
Expand All @@ -24,6 +26,7 @@ use deno_ast::ModuleSpecifier;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::unsync::spawn;
use deno_core::unsync::JoinHandle;
use deno_core::url::Url;
Expand Down Expand Up @@ -267,6 +270,10 @@ async fn sync_resolution_with_fs(
fs::create_dir_all(&deno_node_modules_dir).with_context(|| {
format!("Creating '{}'", deno_local_registry_dir.display())
})?;
let bin_node_modules_dir_path = root_node_modules_dir_path.join(".bin");
fs::create_dir_all(&bin_node_modules_dir_path).with_context(|| {
format!("Creating '{}'", bin_node_modules_dir_path.display())
})?;

let single_process_lock = LaxSingleProcessFsFlag::lock(
deno_local_registry_dir.join(".deno.lock"),
Expand All @@ -291,6 +298,9 @@ async fn sync_resolution_with_fs(
Vec::with_capacity(package_partitions.packages.len());
let mut newest_packages_by_name: HashMap<&String, &NpmResolutionPackage> =
HashMap::with_capacity(package_partitions.packages.len());
let bin_entries_to_setup = Arc::new(Mutex::new(Vec::with_capacity(16)));
let packages_with_install_scripts = Arc::new(Mutex::new(Vec::with_capacity(16)));

for package in &package_partitions.packages {
if let Some(current_pkg) =
newest_packages_by_name.get_mut(&package.id.nv.name)
Expand Down Expand Up @@ -319,6 +329,8 @@ async fn sync_resolution_with_fs(
let cache = cache.clone();
let registry_url = registry_url.clone();
let package = package.clone();
let bin_entries_to_setup = bin_entries_to_setup.clone();
let packages_with_install_scripts = packages_with_install_scripts.clone();
let handle = spawn(async move {
cache
.ensure_package(&package.id.nv, &package.dist, &registry_url)
Expand All @@ -342,6 +354,15 @@ async fn sync_resolution_with_fs(
}
// write out a file that indicates this folder has been initialized
fs::write(initialized_file, "")?;

if package.bin.is_some() {
bin_entries_to_setup.lock().push((package.clone(), package_path.clone()));
}

if package.scripts.contains_key("install") || package.scripts.contains_key("postinstall") {
packages_with_install_scripts.lock().push((package.clone(), package_path));
}

// finally stop showing the progress bar
drop(pb_guard); // explicit for clarity
Ok(())
Expand Down Expand Up @@ -472,6 +493,74 @@ async fn sync_resolution_with_fs(
}
}


// 6. Set up `node_modules/.bin` entries for packages that need it.
for (package, package_path) in &*bin_entries_to_setup.lock() {
let package = snapshot.package_from_id(&package.id).unwrap();
if let Some(bin_entries) = &package.bin {
match bin_entries {
deno_npm::registry::NpmPackageVersionBinEntry::String(script) => {
let name = &package.id.nv.name;
symlink_bin_entry(
name,
script,
&package_path,
&bin_node_modules_dir_path,
)?;
}
deno_npm::registry::NpmPackageVersionBinEntry::Map(entries) => {
for (name, script) in entries {
symlink_bin_entry(
name,
script,
&package_path,
&bin_node_modules_dir_path,
)?;
}
}
}
}
}

// 7. Run pre/post/install scripts for packages
for (package, package_path) in &*packages_with_install_scripts.lock() {
let package = snapshot.package_from_id(&package.id).unwrap();

for (script_name, script) in &package.scripts {
if script_name == "preinstall" || script_name == "install" || script_name == "postinstall" {
log::warn!(
"⚠️ {} {} has a \"{}\" script that might be required to execute for the package to work correctly.",
deno_terminal::colors::yellow("Warning"),
deno_terminal::colors::green(format!("{}", package.id.nv.name)),
deno_terminal::colors::gray(format!("{}", script_name)),
);
log::warn!(" Script: {}", deno_terminal::colors::gray(script));
// TODO: add a prompt here to ask if we should run the script.
log::warn!(" Do you want to run this script with full permissions? [y/N]");
// ASCII code for the bell character.
print!("\x07");
eprint!(" > ");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could a previously executing script stuff stdin? We should maybe re-use the permission prompt code here somehow?

let mut buf = String::new();
let mut should_run = false;
loop {
let line_result = std::io::stdin().read_line(&mut buf);
if let Ok(_nread) = line_result {
let answer = buf.trim();
if answer == "y" || answer == "Y" {
should_run = true;
break;
} else if answer == "n" || answer == "N" {
break;
}
}
}
// if should_run {

// }
}
}
}

setup_cache.save();
drop(single_process_lock);
drop(pb_clear_guard);
Expand Down Expand Up @@ -648,6 +737,39 @@ fn get_package_folder_id_from_folder_name(
})
}

fn symlink_bin_entry(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some code somewhere else that handles doing a symlink like this on unix and will use junctions on windows. It would be good to reuse that code here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or actually, I guess since this is setting up scripts for windows we should have it create something executable here for that.

bin_name: &str,
bin_script: &str,
package_path: &Path,
bin_node_modules_dir_path: &Path,
) -> Result<(), AnyError> {
let link = bin_node_modules_dir_path.join(bin_name);
let original = package_path.join(bin_script);

// Don't bother setting up another link if it already exists
if link.exists() {
let resolved = std::fs::read_link(&link).ok();
if let Some(resolved) = resolved {
if resolved != original {
log::warn!(
"{} Trying to set up '{}' bin for \"{}\", but an entry pointing to \"{}\" already exists. Skipping...",
deno_terminal::colors::yellow("Warning"),
bin_name,
resolved.display(),
original.display()
);
return Ok(());
}
}
}

// TODO: handle Windows
symlink(&original, &link).with_context(|| {
format!("Can't set up '{}' bin at {}", bin_name, link.display())
})?;
Ok(())
}

fn symlink_package_dir(
old_path: &Path,
new_path: &Path,
Expand Down
14 changes: 13 additions & 1 deletion cli/tools/installer.rs
Expand Up @@ -253,6 +253,18 @@ pub fn uninstall(uninstall_flags: UninstallFlags) -> Result<(), AnyError> {
Ok(())
}

async fn install_local(flags: Flags) -> Result<(), AnyError> {
let factory = CliFactory::from_flags(flags)?;
let cli_options = factory.cli_options();
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?;
npm_resolver.resolve_pending().await?;
}
Ok(())
}

pub async fn install_command(
flags: Flags,
install_flags: InstallFlags,
Expand All @@ -263,7 +275,7 @@ pub async fn install_command(

let install_flags_global = match install_flags.kind {
InstallKind::Global(flags) => flags,
InstallKind::Local => unreachable!(),
InstallKind::Local => return install_local(flags).await,
};

// ensure the module is cached
Expand Down