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

perf: Optimize installing npm deps on mac [WIP] #23573

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
85 changes: 56 additions & 29 deletions cli/args/flags.rs
Expand Up @@ -32,6 +32,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 @@ -802,8 +803,15 @@ impl Flags {
std::env::current_dir().ok()
}
Add(_) | Bundle(_) | Completions(_) | Doc(_) | Fmt(_) | Init(_)
| Install(_) | Uninstall(_) | Jupyter(_) | Lsp | Lint(_) | Types
| Upgrade(_) | Vendor(_) => None,
| Uninstall(_) | Jupyter(_) | Lsp | Lint(_) | Types | Upgrade(_)
| Vendor(_) => None,
Install(_) => {
if *DENO_FUTURE {
std::env::current_dir().ok()
} else {
None
}
}
}
}

Expand Down Expand Up @@ -1979,25 +1987,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 @@ -2007,6 +2026,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 @@ -3719,28 +3739,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 || !*DENO_FUTURE {
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 @@ -717,7 +717,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>,
Copy link
Member

Choose a reason for hiding this comment

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

There's a readonly method maybe_package_json() that could be used instead.

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 @@ -407,7 +407,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
13 changes: 12 additions & 1 deletion cli/tools/installer.rs
Expand Up @@ -253,6 +253,17 @@ pub fn uninstall(uninstall_flags: UninstallFlags) -> Result<(), AnyError> {
Ok(())
}

async fn install_local(flags: Flags) -> Result<(), AnyError> {
let factory = CliFactory::from_flags(flags)?;
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 +274,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
44 changes: 42 additions & 2 deletions cli/util/fs.rs
Expand Up @@ -529,6 +529,46 @@ pub fn copy_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> {
Ok(())
}

mod imp {
#[cfg(target_vendor = "apple")]
pub use darwin::copy_file;

#[cfg(not(target_vendor = "apple"))]
pub use non_darwin::copy_file;

#[cfg(target_vendor = "apple")]
mod darwin {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;

use super::super::*;
pub fn copy_file(from: &Path, to: &Path) -> Result<(), std::io::Error> {
let from_c = CString::new(from.as_os_str().as_bytes()).unwrap();
let to_c = CString::new(to.as_os_str().as_bytes()).unwrap();
let result =
unsafe { libc::clonefile(from_c.as_ptr(), to_c.as_ptr(), 0) };
if result == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
}

#[cfg(all(not(target_vendor = "apple")))]
mod non_darwin {
use super::super::*;

pub fn copy_file(from: &Path, to: &Path) -> Result<(), std::io::Error> {
std::fs::hard_link(from, to)
}
}
}

fn fast_copy_file(from: &Path, to: &Path) -> Result<(), std::io::Error> {
imp::copy_file(from, to)
}

/// Hardlinks the files in one directory to another directory.
///
/// Note: Does not handle symlinks.
Expand All @@ -554,7 +594,7 @@ pub fn hard_link_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> {
// a way to hard link with overwriting in Rust, but maybe there is some
// way with platform specific code. The workaround here is to handle
// scenarios where something else might create or remove files.
if let Err(err) = std::fs::hard_link(&new_from, &new_to) {
if let Err(err) = fast_copy_file(&new_from, &new_to) {
if err.kind() == ErrorKind::AlreadyExists {
if let Err(err) = std::fs::remove_file(&new_to) {
if err.kind() == ErrorKind::NotFound {
Expand All @@ -575,7 +615,7 @@ pub fn hard_link_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> {

// Always attempt to recreate the hardlink. In contention scenarios, the other process
// might have been killed or exited after removing the file, but before creating the hardlink
if let Err(err) = std::fs::hard_link(&new_from, &new_to) {
if let Err(err) = fast_copy_file(&new_from, &new_to) {
// Assume another process/thread created this hard link to the file we are wanting
// to now create then sleep a little bit to let the other process/thread move ahead
// faster to reduce contention.
Expand Down
12 changes: 12 additions & 0 deletions tests/specs/install/future_install_global/__test__.jsonc
@@ -0,0 +1,12 @@
{
"tempDir": true,
"envs": {
"DENO_FUTURE": "1"
},
"steps": [
{
"args": "install --global --root ./bins ./main.js",
"output": "install.out"
}
]
}
5 changes: 5 additions & 0 deletions tests/specs/install/future_install_global/install.out
@@ -0,0 +1,5 @@
Download http://localhost:4545/npm/registry/@denotest/esm-basic
Download http://localhost:4545/npm/registry/@denotest/esm-basic/1.0.0.tgz
Initialize @denotest/esm-basic@1.0.0
✅ Successfully installed deno-cli-test[WILDCARD]
[WILDCARD]
3 changes: 3 additions & 0 deletions tests/specs/install/future_install_global/main.js
@@ -0,0 +1,3 @@
import { setValue } from "@denotest/esm-basic";

setValue(5);
Empty file.
7 changes: 7 additions & 0 deletions tests/specs/install/future_install_global/package.json
@@ -0,0 +1,7 @@
{
"name": "deno-test-bin",
"dependencies": {
"@denotest/esm-basic": "*"
},
"type": "module"
}
19 changes: 19 additions & 0 deletions tests/specs/install/future_install_node_modules/__test__.jsonc

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

4 changes: 4 additions & 0 deletions tests/specs/install/future_install_node_modules/install.out

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

3 changes: 3 additions & 0 deletions tests/specs/install/future_install_node_modules/main.js

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

Empty file.
6 changes: 6 additions & 0 deletions tests/specs/install/future_install_node_modules/package.json

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

9 changes: 9 additions & 0 deletions tests/specs/install/no_future_install_global/__test__.jsonc
@@ -0,0 +1,9 @@
{
"tempDir": true,
"steps": [
{
"args": "install --root ./bins ./main.js",
"output": "install.out"
}
]
}
5 changes: 5 additions & 0 deletions tests/specs/install/no_future_install_global/install.out
@@ -0,0 +1,5 @@
⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.
Download http://localhost:4545/npm/registry/@denotest/esm-basic
Download http://localhost:4545/npm/registry/@denotest/esm-basic/1.0.0.tgz
✅ Successfully installed deno-cli-test[WILDCARD]
[WILDCARD]
3 changes: 3 additions & 0 deletions tests/specs/install/no_future_install_global/main.js
@@ -0,0 +1,3 @@
import { setValue } from "npm:@denotest/esm-basic";

setValue(5);
Empty file.
7 changes: 7 additions & 0 deletions tests/specs/install/no_future_install_global/package.json
@@ -0,0 +1,7 @@
{
"name": "deno-test-bin",
"dependencies": {
"@denotest/esm-basic": "*"
},
"type": "module"
}