mirror of
https://github.com/denoland/deno.git
synced 2024-12-20 14:24:48 -05:00
6f506208f6
Currently deno eagerly caches all npm packages in the workspace's npm resolution. So, for instance, running a file `foo.ts` that imports `npm:chalk` will also install all dependencies listed in `package.json` and all `npm` dependencies listed in the lockfile. This PR refactors things to give more control over when and what npm packages are automatically cached while building the module graph. After this PR, by default the current behavior is unchanged _except_ for `deno install --entrypoint`, which will only cache npm packages used by the given entrypoint. For the other subcommands, this behavior can be enabled with `--unstable-npm-lazy-caching` Fixes #25782. --------- Signed-off-by: Nathan Whitaker <17734409+nathanwhit@users.noreply.github.com> Co-authored-by: Luca Casonato <hello@lcas.dev>
495 lines
14 KiB
Rust
495 lines
14 KiB
Rust
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use crate::args::check_warn_tsconfig;
|
|
use crate::args::CompileFlags;
|
|
use crate::args::Flags;
|
|
use crate::factory::CliFactory;
|
|
use crate::http_util::HttpClientProvider;
|
|
use crate::standalone::binary::StandaloneRelativeFileBaseUrl;
|
|
use crate::standalone::is_standalone_binary;
|
|
use deno_ast::MediaType;
|
|
use deno_ast::ModuleSpecifier;
|
|
use deno_core::anyhow::bail;
|
|
use deno_core::anyhow::Context;
|
|
use deno_core::error::generic_error;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::resolve_url_or_path;
|
|
use deno_graph::GraphKind;
|
|
use deno_terminal::colors;
|
|
use rand::Rng;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use super::installer::infer_name_from_url;
|
|
|
|
pub async fn compile(
|
|
flags: Arc<Flags>,
|
|
compile_flags: CompileFlags,
|
|
) -> Result<(), AnyError> {
|
|
let factory = CliFactory::from_flags(flags);
|
|
let cli_options = factory.cli_options()?;
|
|
let module_graph_creator = factory.module_graph_creator().await?;
|
|
let binary_writer = factory.create_compile_binary_writer().await?;
|
|
let http_client = factory.http_client_provider();
|
|
let entrypoint = cli_options.resolve_main_module()?;
|
|
let (module_roots, include_files) = get_module_roots_and_include_files(
|
|
entrypoint,
|
|
&compile_flags,
|
|
cli_options.initial_cwd(),
|
|
)?;
|
|
|
|
// this is not supported, so show a warning about it, but don't error in order
|
|
// to allow someone to still run `deno compile` when this is in a deno.json
|
|
if cli_options.unstable_sloppy_imports() {
|
|
log::warn!(
|
|
concat!(
|
|
"{} Sloppy imports are not supported in deno compile. ",
|
|
"The compiled executable may encounter runtime errors.",
|
|
),
|
|
crate::colors::yellow("Warning"),
|
|
);
|
|
}
|
|
|
|
let output_path = resolve_compile_executable_output_path(
|
|
http_client,
|
|
&compile_flags,
|
|
cli_options.initial_cwd(),
|
|
)
|
|
.await?;
|
|
|
|
let graph = Arc::try_unwrap(
|
|
module_graph_creator
|
|
.create_graph_and_maybe_check(module_roots.clone())
|
|
.await?,
|
|
)
|
|
.unwrap();
|
|
let graph = if cli_options.type_check_mode().is_true() {
|
|
// In this case, the previous graph creation did type checking, which will
|
|
// create a module graph with types information in it. We don't want to
|
|
// store that in the binary so create a code only module graph from scratch.
|
|
module_graph_creator
|
|
.create_graph(
|
|
GraphKind::CodeOnly,
|
|
module_roots,
|
|
crate::graph_util::NpmCachingStrategy::Eager,
|
|
)
|
|
.await?
|
|
} else {
|
|
graph
|
|
};
|
|
|
|
let ts_config_for_emit = cli_options
|
|
.resolve_ts_config_for_emit(deno_config::deno_json::TsConfigType::Emit)?;
|
|
check_warn_tsconfig(&ts_config_for_emit);
|
|
let root_dir_url = resolve_root_dir_from_specifiers(
|
|
cli_options.workspace().root_dir(),
|
|
graph
|
|
.specifiers()
|
|
.map(|(s, _)| s)
|
|
.chain(
|
|
cli_options
|
|
.node_modules_dir_path()
|
|
.and_then(|p| ModuleSpecifier::from_directory_path(p).ok())
|
|
.iter(),
|
|
)
|
|
.chain(include_files.iter()),
|
|
);
|
|
log::debug!("Binary root dir: {}", root_dir_url);
|
|
log::info!(
|
|
"{} {} to {}",
|
|
colors::green("Compile"),
|
|
entrypoint,
|
|
output_path.display(),
|
|
);
|
|
validate_output_path(&output_path)?;
|
|
|
|
let mut temp_filename = output_path.file_name().unwrap().to_owned();
|
|
temp_filename.push(format!(
|
|
".tmp-{}",
|
|
faster_hex::hex_encode(
|
|
&rand::thread_rng().gen::<[u8; 8]>(),
|
|
&mut [0u8; 16]
|
|
)
|
|
.unwrap()
|
|
));
|
|
let temp_path = output_path.with_file_name(temp_filename);
|
|
|
|
let file = std::fs::File::create(&temp_path).with_context(|| {
|
|
format!("Opening temporary file '{}'", temp_path.display())
|
|
})?;
|
|
|
|
let write_result = binary_writer
|
|
.write_bin(
|
|
file,
|
|
&graph,
|
|
StandaloneRelativeFileBaseUrl::from(&root_dir_url),
|
|
entrypoint,
|
|
&include_files,
|
|
&compile_flags,
|
|
)
|
|
.await
|
|
.with_context(|| {
|
|
format!(
|
|
"Writing deno compile executable to temporary file '{}'",
|
|
temp_path.display()
|
|
)
|
|
});
|
|
|
|
// set it as executable
|
|
#[cfg(unix)]
|
|
let write_result = write_result.and_then(|_| {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let perms = std::fs::Permissions::from_mode(0o755);
|
|
std::fs::set_permissions(&temp_path, perms).with_context(|| {
|
|
format!(
|
|
"Setting permissions on temporary file '{}'",
|
|
temp_path.display()
|
|
)
|
|
})
|
|
});
|
|
|
|
let write_result = write_result.and_then(|_| {
|
|
std::fs::rename(&temp_path, &output_path).with_context(|| {
|
|
format!(
|
|
"Renaming temporary file '{}' to '{}'",
|
|
temp_path.display(),
|
|
output_path.display()
|
|
)
|
|
})
|
|
});
|
|
|
|
if let Err(err) = write_result {
|
|
// errored, so attempt to remove the temporary file
|
|
let _ = std::fs::remove_file(temp_path);
|
|
return Err(err);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// This function writes out a final binary to specified path. If output path
|
|
/// is not already standalone binary it will return error instead.
|
|
fn validate_output_path(output_path: &Path) -> Result<(), AnyError> {
|
|
if output_path.exists() {
|
|
// If the output is a directory, throw error
|
|
if output_path.is_dir() {
|
|
bail!(
|
|
concat!(
|
|
"Could not compile to file '{}' because a directory exists with ",
|
|
"the same name. You can use the `--output <file-path>` flag to ",
|
|
"provide an alternative name."
|
|
),
|
|
output_path.display()
|
|
);
|
|
}
|
|
|
|
// Make sure we don't overwrite any file not created by Deno compiler because
|
|
// this filename is chosen automatically in some cases.
|
|
if !is_standalone_binary(output_path) {
|
|
bail!(
|
|
concat!(
|
|
"Could not compile to file '{}' because the file already exists ",
|
|
"and cannot be overwritten. Please delete the existing file or ",
|
|
"use the `--output <file-path>` flag to provide an alternative name."
|
|
),
|
|
output_path.display()
|
|
);
|
|
}
|
|
|
|
// Remove file if it was indeed a deno compiled binary, to avoid corruption
|
|
// (see https://github.com/denoland/deno/issues/10310)
|
|
std::fs::remove_file(output_path)?;
|
|
} else {
|
|
let output_base = &output_path.parent().unwrap();
|
|
if output_base.exists() && output_base.is_file() {
|
|
bail!(
|
|
concat!(
|
|
"Could not compile to file '{}' because its parent directory ",
|
|
"is an existing file. You can use the `--output <file-path>` flag to ",
|
|
"provide an alternative name.",
|
|
),
|
|
output_base.display(),
|
|
);
|
|
}
|
|
std::fs::create_dir_all(output_base)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_module_roots_and_include_files(
|
|
entrypoint: &ModuleSpecifier,
|
|
compile_flags: &CompileFlags,
|
|
initial_cwd: &Path,
|
|
) -> Result<(Vec<ModuleSpecifier>, Vec<ModuleSpecifier>), AnyError> {
|
|
fn is_module_graph_module(url: &ModuleSpecifier) -> bool {
|
|
if url.scheme() != "file" {
|
|
return true;
|
|
}
|
|
let media_type = MediaType::from_specifier(url);
|
|
match media_type {
|
|
MediaType::JavaScript
|
|
| MediaType::Jsx
|
|
| MediaType::Mjs
|
|
| MediaType::Cjs
|
|
| MediaType::TypeScript
|
|
| MediaType::Mts
|
|
| MediaType::Cts
|
|
| MediaType::Dts
|
|
| MediaType::Dmts
|
|
| MediaType::Dcts
|
|
| MediaType::Tsx
|
|
| MediaType::Json
|
|
| MediaType::Wasm => true,
|
|
MediaType::Css | MediaType::SourceMap | MediaType::Unknown => false,
|
|
}
|
|
}
|
|
|
|
let mut module_roots = Vec::with_capacity(compile_flags.include.len() + 1);
|
|
let mut include_files = Vec::with_capacity(compile_flags.include.len());
|
|
module_roots.push(entrypoint.clone());
|
|
for side_module in &compile_flags.include {
|
|
let url = resolve_url_or_path(side_module, initial_cwd)?;
|
|
if is_module_graph_module(&url) {
|
|
module_roots.push(url);
|
|
} else {
|
|
include_files.push(url);
|
|
}
|
|
}
|
|
Ok((module_roots, include_files))
|
|
}
|
|
|
|
async fn resolve_compile_executable_output_path(
|
|
http_client_provider: &HttpClientProvider,
|
|
compile_flags: &CompileFlags,
|
|
current_dir: &Path,
|
|
) -> Result<PathBuf, AnyError> {
|
|
let module_specifier =
|
|
resolve_url_or_path(&compile_flags.source_file, current_dir)?;
|
|
|
|
let output_flag = compile_flags.output.clone();
|
|
let mut output_path = if let Some(out) = output_flag.as_ref() {
|
|
let mut out_path = PathBuf::from(out);
|
|
if out.ends_with('/') || out.ends_with('\\') {
|
|
if let Some(infer_file_name) =
|
|
infer_name_from_url(http_client_provider, &module_specifier)
|
|
.await
|
|
.map(PathBuf::from)
|
|
{
|
|
out_path = out_path.join(infer_file_name);
|
|
}
|
|
} else {
|
|
out_path = out_path.to_path_buf();
|
|
}
|
|
Some(out_path)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if output_flag.is_none() {
|
|
output_path = infer_name_from_url(http_client_provider, &module_specifier)
|
|
.await
|
|
.map(PathBuf::from)
|
|
}
|
|
|
|
output_path.ok_or_else(|| generic_error(
|
|
"An executable name was not provided. One could not be inferred from the URL. Aborting.",
|
|
)).map(|output_path| {
|
|
get_os_specific_filepath(output_path, &compile_flags.target)
|
|
})
|
|
}
|
|
|
|
fn get_os_specific_filepath(
|
|
output: PathBuf,
|
|
target: &Option<String>,
|
|
) -> PathBuf {
|
|
let is_windows = match target {
|
|
Some(target) => target.contains("windows"),
|
|
None => cfg!(windows),
|
|
};
|
|
if is_windows && output.extension().unwrap_or_default() != "exe" {
|
|
if let Some(ext) = output.extension() {
|
|
// keep version in my-exe-0.1.0 -> my-exe-0.1.0.exe
|
|
output.with_extension(format!("{}.exe", ext.to_string_lossy()))
|
|
} else {
|
|
output.with_extension("exe")
|
|
}
|
|
} else {
|
|
output
|
|
}
|
|
}
|
|
|
|
fn resolve_root_dir_from_specifiers<'a>(
|
|
starting_dir: &ModuleSpecifier,
|
|
specifiers: impl Iterator<Item = &'a ModuleSpecifier>,
|
|
) -> ModuleSpecifier {
|
|
fn select_common_root<'a>(a: &'a str, b: &'a str) -> &'a str {
|
|
let min_length = a.len().min(b.len());
|
|
|
|
let mut last_slash = 0;
|
|
for i in 0..min_length {
|
|
if a.as_bytes()[i] == b.as_bytes()[i] && a.as_bytes()[i] == b'/' {
|
|
last_slash = i;
|
|
} else if a.as_bytes()[i] != b.as_bytes()[i] {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Return the common root path up to the last common slash.
|
|
// This returns a slice of the original string 'a', up to and including the last matching '/'.
|
|
let common = &a[..=last_slash];
|
|
if cfg!(windows) && common == "file:///" {
|
|
a
|
|
} else {
|
|
common
|
|
}
|
|
}
|
|
|
|
fn is_file_system_root(url: &str) -> bool {
|
|
let Some(path) = url.strip_prefix("file:///") else {
|
|
return false;
|
|
};
|
|
if cfg!(windows) {
|
|
let Some((_drive, path)) = path.split_once('/') else {
|
|
return true;
|
|
};
|
|
path.is_empty()
|
|
} else {
|
|
path.is_empty()
|
|
}
|
|
}
|
|
|
|
let mut found_dir = starting_dir.as_str();
|
|
if !is_file_system_root(found_dir) {
|
|
for specifier in specifiers {
|
|
if specifier.scheme() == "file" {
|
|
found_dir = select_common_root(found_dir, specifier.as_str());
|
|
}
|
|
}
|
|
}
|
|
let found_dir = if is_file_system_root(found_dir) {
|
|
found_dir
|
|
} else {
|
|
// include the parent dir name because it helps create some context
|
|
found_dir
|
|
.strip_suffix('/')
|
|
.unwrap_or(found_dir)
|
|
.rfind('/')
|
|
.map(|i| &found_dir[..i + 1])
|
|
.unwrap_or(found_dir)
|
|
};
|
|
ModuleSpecifier::parse(found_dir).unwrap()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
pub use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn resolve_compile_executable_output_path_target_linux() {
|
|
let http_client = HttpClientProvider::new(None, None);
|
|
let path = resolve_compile_executable_output_path(
|
|
&http_client,
|
|
&CompileFlags {
|
|
source_file: "mod.ts".to_string(),
|
|
output: Some(String::from("./file")),
|
|
args: Vec::new(),
|
|
target: Some("x86_64-unknown-linux-gnu".to_string()),
|
|
no_terminal: false,
|
|
icon: None,
|
|
include: vec![],
|
|
},
|
|
&std::env::current_dir().unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// no extension, no matter what the operating system is
|
|
// because the target was specified as linux
|
|
// https://github.com/denoland/deno/issues/9667
|
|
assert_eq!(path.file_name().unwrap(), "file");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn resolve_compile_executable_output_path_target_windows() {
|
|
let http_client = HttpClientProvider::new(None, None);
|
|
let path = resolve_compile_executable_output_path(
|
|
&http_client,
|
|
&CompileFlags {
|
|
source_file: "mod.ts".to_string(),
|
|
output: Some(String::from("./file")),
|
|
args: Vec::new(),
|
|
target: Some("x86_64-pc-windows-msvc".to_string()),
|
|
include: vec![],
|
|
icon: None,
|
|
no_terminal: false,
|
|
},
|
|
&std::env::current_dir().unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(path.file_name().unwrap(), "file.exe");
|
|
}
|
|
|
|
#[test]
|
|
fn test_os_specific_file_path() {
|
|
fn run_test(path: &str, target: Option<&str>, expected: &str) {
|
|
assert_eq!(
|
|
get_os_specific_filepath(
|
|
PathBuf::from(path),
|
|
&target.map(|s| s.to_string())
|
|
),
|
|
PathBuf::from(expected)
|
|
);
|
|
}
|
|
|
|
if cfg!(windows) {
|
|
run_test("C:\\my-exe", None, "C:\\my-exe.exe");
|
|
run_test("C:\\my-exe.exe", None, "C:\\my-exe.exe");
|
|
run_test("C:\\my-exe-0.1.2", None, "C:\\my-exe-0.1.2.exe");
|
|
} else {
|
|
run_test("my-exe", Some("linux"), "my-exe");
|
|
run_test("my-exe-0.1.2", Some("linux"), "my-exe-0.1.2");
|
|
}
|
|
|
|
run_test("C:\\my-exe", Some("windows"), "C:\\my-exe.exe");
|
|
run_test("C:\\my-exe.exe", Some("windows"), "C:\\my-exe.exe");
|
|
run_test("C:\\my-exe.0.1.2", Some("windows"), "C:\\my-exe.0.1.2.exe");
|
|
run_test("my-exe-0.1.2", Some("linux"), "my-exe-0.1.2");
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_root_dir_from_specifiers() {
|
|
fn resolve(start: &str, specifiers: &[&str]) -> String {
|
|
let specifiers = specifiers
|
|
.iter()
|
|
.map(|s| ModuleSpecifier::parse(s).unwrap())
|
|
.collect::<Vec<_>>();
|
|
resolve_root_dir_from_specifiers(
|
|
&ModuleSpecifier::parse(start).unwrap(),
|
|
specifiers.iter(),
|
|
)
|
|
.to_string()
|
|
}
|
|
|
|
assert_eq!(resolve("file:///a/b/c", &["file:///a/b/c/d"]), "file:///a/");
|
|
assert_eq!(
|
|
resolve("file:///a/b/c/", &["file:///a/b/c/d"]),
|
|
"file:///a/b/"
|
|
);
|
|
assert_eq!(
|
|
resolve("file:///a/b/c/", &["file:///a/b/c/d", "file:///a/b/c/e"]),
|
|
"file:///a/b/"
|
|
);
|
|
assert_eq!(resolve("file:///", &["file:///a/b/c/d"]), "file:///");
|
|
if cfg!(windows) {
|
|
assert_eq!(resolve("file:///c:/", &["file:///c:/test"]), "file:///c:/");
|
|
// this will ignore the other one because it's on a separate drive
|
|
assert_eq!(
|
|
resolve("file:///c:/a/b/c/", &["file:///v:/a/b/c/d"]),
|
|
"file:///c:/a/b/"
|
|
);
|
|
}
|
|
}
|
|
}
|