1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-18 05:14:21 -05:00

fix(compile): output contents of embedded file system (#27302)

This commit is contained in:
David Sherret 2024-12-11 09:40:50 -05:00 committed by GitHub
parent 9df6be8916
commit c6fa62896d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 344 additions and 136 deletions

View file

@ -87,6 +87,7 @@ use super::serialization::DenoCompileModuleData;
use super::serialization::DeserializedDataSection;
use super::serialization::RemoteModulesStore;
use super::serialization::RemoteModulesStoreBuilder;
use super::virtual_fs::output_vfs;
use super::virtual_fs::FileBackedVfs;
use super::virtual_fs::VfsBuilder;
use super::virtual_fs::VfsFileSubDataKind;
@ -367,6 +368,16 @@ pub fn extract_standalone(
}))
}
pub struct WriteBinOptions<'a> {
pub writer: File,
pub display_output_filename: &'a str,
pub graph: &'a ModuleGraph,
pub root_dir_url: StandaloneRelativeFileBaseUrl<'a>,
pub entrypoint: &'a ModuleSpecifier,
pub include_files: &'a [ModuleSpecifier],
pub compile_flags: &'a CompileFlags,
}
pub struct DenoCompileBinaryWriter<'a> {
cjs_tracker: &'a CjsTracker,
cli_options: &'a CliOptions,
@ -407,18 +418,14 @@ impl<'a> DenoCompileBinaryWriter<'a> {
pub async fn write_bin(
&self,
writer: File,
graph: &ModuleGraph,
root_dir_url: StandaloneRelativeFileBaseUrl<'_>,
entrypoint: &ModuleSpecifier,
include_files: &[ModuleSpecifier],
compile_flags: &CompileFlags,
options: WriteBinOptions<'_>,
) -> Result<(), AnyError> {
// Select base binary based on target
let mut original_binary = self.get_base_binary(compile_flags).await?;
let mut original_binary =
self.get_base_binary(options.compile_flags).await?;
if compile_flags.no_terminal {
let target = compile_flags.resolve_target();
if options.compile_flags.no_terminal {
let target = options.compile_flags.resolve_target();
if !target.contains("windows") {
bail!(
"The `--no-terminal` flag is only available when targeting Windows (current: {})",
@ -428,8 +435,8 @@ impl<'a> DenoCompileBinaryWriter<'a> {
set_windows_binary_to_gui(&mut original_binary)
.context("Setting windows binary to GUI.")?;
}
if compile_flags.icon.is_some() {
let target = compile_flags.resolve_target();
if options.compile_flags.icon.is_some() {
let target = options.compile_flags.resolve_target();
if !target.contains("windows") {
bail!(
"The `--icon` flag is only available when targeting Windows (current: {})",
@ -437,17 +444,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
)
}
}
self
.write_standalone_binary(
writer,
original_binary,
graph,
root_dir_url,
entrypoint,
include_files,
compile_flags,
)
.await
self.write_standalone_binary(options, original_binary).await
}
async fn get_base_binary(
@ -552,14 +549,18 @@ impl<'a> DenoCompileBinaryWriter<'a> {
#[allow(clippy::too_many_arguments)]
async fn write_standalone_binary(
&self,
writer: File,
options: WriteBinOptions<'_>,
original_bin: Vec<u8>,
graph: &ModuleGraph,
root_dir_url: StandaloneRelativeFileBaseUrl<'_>,
entrypoint: &ModuleSpecifier,
include_files: &[ModuleSpecifier],
compile_flags: &CompileFlags,
) -> Result<(), AnyError> {
let WriteBinOptions {
writer,
display_output_filename,
graph,
root_dir_url,
entrypoint,
include_files,
compile_flags,
} = options;
let ca_data = match self.cli_options.ca_data() {
Some(CaData::File(ca_file)) => Some(
std::fs::read(ca_file).with_context(|| format!("Reading {ca_file}"))?,
@ -784,6 +785,8 @@ impl<'a> DenoCompileBinaryWriter<'a> {
otel_config: self.cli_options.otel_config(),
};
output_vfs(&vfs, display_output_filename);
write_binary_bytes(
writer,
original_bin,

View file

@ -31,6 +31,7 @@ use serde::Serialize;
use thiserror::Error;
use crate::util;
use crate::util::display::DisplayTreeNode;
use crate::util::fs::canonicalize_path;
#[derive(Debug, Copy, Clone)]
@ -51,6 +52,7 @@ pub struct StripRootError {
target: PathBuf,
}
#[derive(Debug)]
pub struct VfsBuilder {
root_path: PathBuf,
root_dir: VirtualDirectory,
@ -364,6 +366,125 @@ impl VfsBuilder {
}
}
pub fn output_vfs(builder: &VfsBuilder, executable_name: &str) {
if !log::log_enabled!(log::Level::Info) {
return; // no need to compute if won't output
}
if builder.root_dir.entries.is_empty() {
return; // nothing to output
}
let mut text = String::new();
let display_tree = vfs_as_display_tree(builder, executable_name);
display_tree.print(&mut text).unwrap(); // unwrap ok because it's writing to a string
log::info!(
"\n{}\n",
deno_terminal::colors::bold("Embedded File System")
);
log::info!("{}\n", text.trim());
}
fn vfs_as_display_tree(
builder: &VfsBuilder,
executable_name: &str,
) -> DisplayTreeNode {
enum EntryOutput<'a> {
All,
Subset(Vec<DirEntryOutput<'a>>),
File,
Symlink(&'a [String]),
}
impl<'a> EntryOutput<'a> {
pub fn as_display_tree(&self, name: String) -> DisplayTreeNode {
DisplayTreeNode {
text: match self {
EntryOutput::All | EntryOutput::Subset(_) | EntryOutput::File => name,
EntryOutput::Symlink(parts) => {
format!("{} --> {}", name, parts.join("/"))
}
},
children: match self {
EntryOutput::All => vec![DisplayTreeNode::from_text("*".to_string())],
EntryOutput::Subset(vec) => vec
.iter()
.map(|e| e.output.as_display_tree(e.name.to_string()))
.collect(),
EntryOutput::File | EntryOutput::Symlink(_) => vec![],
},
}
}
}
pub struct DirEntryOutput<'a> {
name: &'a str,
output: EntryOutput<'a>,
}
fn include_all_entries<'a>(
dir: &Path,
vfs_dir: &'a VirtualDirectory,
) -> EntryOutput<'a> {
EntryOutput::Subset(
vfs_dir
.entries
.iter()
.map(|entry| DirEntryOutput {
name: entry.name(),
output: analyze_entry(&dir.join(entry.name()), entry),
})
.collect(),
)
}
fn analyze_entry<'a>(path: &Path, entry: &'a VfsEntry) -> EntryOutput<'a> {
match entry {
VfsEntry::Dir(virtual_directory) => analyze_dir(path, virtual_directory),
VfsEntry::File(_) => EntryOutput::File,
VfsEntry::Symlink(virtual_symlink) => {
EntryOutput::Symlink(&virtual_symlink.dest_parts)
}
}
}
fn analyze_dir<'a>(
dir: &Path,
vfs_dir: &'a VirtualDirectory,
) -> EntryOutput<'a> {
let real_entry_count = std::fs::read_dir(dir)
.ok()
.map(|entries| entries.flat_map(|e| e.ok()).count())
.unwrap_or(0);
if real_entry_count == vfs_dir.entries.len() {
let children = vfs_dir
.entries
.iter()
.map(|entry| DirEntryOutput {
name: entry.name(),
output: analyze_entry(&dir.join(entry.name()), entry),
})
.collect::<Vec<_>>();
if children
.iter()
.all(|c| !matches!(c.output, EntryOutput::Subset(_)))
{
EntryOutput::All
} else {
EntryOutput::Subset(children)
}
} else {
include_all_entries(dir, vfs_dir)
}
}
// always include all the entries for the root directory, otherwise the
// user might not have context about what's being shown
let output = include_all_entries(&builder.root_path, &builder.root_dir);
output
.as_display_tree(deno_terminal::colors::italic(executable_name).to_string())
}
#[derive(Debug)]
enum VfsEntryRef<'a> {
Dir(&'a VirtualDirectory),
@ -1013,6 +1134,7 @@ impl FileBackedVfs {
#[cfg(test)]
mod test {
use console_static_text::ansi::strip_ansi_codes;
use std::io::Write;
use test_util::TempDir;
@ -1299,4 +1421,50 @@ mod test {
.unwrap();
assert_eq!(all_buf.to_vec(), b"123456789");
}
#[test]
fn test_vfs_as_display_tree() {
let temp_dir = TempDir::new();
temp_dir.write("root.txt", "");
temp_dir.create_dir_all("a");
temp_dir.write("a/a.txt", "");
temp_dir.write("a/b.txt", "");
temp_dir.create_dir_all("b");
temp_dir.write("b/a.txt", "");
temp_dir.write("b/b.txt", "");
temp_dir.create_dir_all("c");
temp_dir.write("c/a.txt", "contents");
temp_dir.symlink_file("c/a.txt", "c/b.txt");
assert_eq!(temp_dir.read_to_string("c/b.txt"), "contents"); // ensure the symlink works
let mut vfs_builder =
VfsBuilder::new(temp_dir.path().to_path_buf()).unwrap();
// full dir
vfs_builder
.add_dir_recursive(temp_dir.path().join("a").as_path())
.unwrap();
// part of the dir
vfs_builder
.add_file_at_path(temp_dir.path().join("b/a.txt").as_path())
.unwrap();
// symlink
vfs_builder
.add_dir_recursive(temp_dir.path().join("c").as_path())
.unwrap();
temp_dir.write("c/c.txt", ""); // write an extra file so it shows the whole directory
let node = vfs_as_display_tree(&vfs_builder, "executable");
let mut text = String::new();
node.print(&mut text).unwrap();
assert_eq!(
strip_ansi_codes(&text),
r#"executable
a
*
b
a.txt
c
a.txt
b.txt --> c/a.txt
"#
);
}
}

View file

@ -6,6 +6,7 @@ use crate::args::Flags;
use crate::factory::CliFactory;
use crate::http_util::HttpClientProvider;
use crate::standalone::binary::StandaloneRelativeFileBaseUrl;
use crate::standalone::binary::WriteBinOptions;
use crate::standalone::is_standalone_binary;
use deno_ast::MediaType;
use deno_ast::ModuleSpecifier;
@ -15,6 +16,7 @@ use deno_core::error::generic_error;
use deno_core::error::AnyError;
use deno_core::resolve_url_or_path;
use deno_graph::GraphKind;
use deno_path_util::url_from_file_path;
use deno_terminal::colors;
use rand::Rng;
use std::path::Path;
@ -93,7 +95,16 @@ pub async fn compile(
.and_then(|p| ModuleSpecifier::from_directory_path(p).ok())
.iter(),
)
.chain(include_files.iter()),
.chain(include_files.iter())
.chain(
// sometimes the import map path is outside the root dir
cli_options
.workspace()
.to_import_map_path()
.ok()
.and_then(|p| p.and_then(|p| url_from_file_path(&p).ok()))
.iter(),
),
);
log::debug!("Binary root dir: {}", root_dir_url);
log::info!(
@ -120,14 +131,18 @@ pub async fn compile(
})?;
let write_result = binary_writer
.write_bin(
file,
&graph,
StandaloneRelativeFileBaseUrl::from(&root_dir_url),
.write_bin(WriteBinOptions {
writer: file,
display_output_filename: &output_path
.file_name()
.unwrap()
.to_string_lossy(),
graph: &graph,
root_dir_url: StandaloneRelativeFileBaseUrl::from(&root_dir_url),
entrypoint,
&include_files,
&compile_flags,
)
include_files: &include_files,
compile_flags: &compile_flags,
})
.await
.with_context(|| {
format!(
@ -368,17 +383,6 @@ fn resolve_root_dir_from_specifiers<'a>(
}
}
}
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()
}
@ -473,14 +477,17 @@ mod test {
.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"]),
resolve("file:///a/b/e", &["file:///a/b/c/d"]),
"file:///a/b/"
);
assert_eq!(
resolve("file:///a/b/c/", &["file:///a/b/c/d"]),
"file:///a/b/c/"
);
assert_eq!(
resolve("file:///a/b/c/", &["file:///a/b/c/d", "file:///a/b/c/e"]),
"file:///a/b/"
"file:///a/b/c/"
);
assert_eq!(resolve("file:///", &["file:///a/b/c/d"]), "file:///");
if cfg!(windows) {
@ -488,7 +495,7 @@ mod test {
// 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/"
"file:///c:/a/b/c/"
);
}
}

View file

@ -2,7 +2,6 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt;
use std::fmt::Write;
use std::sync::Arc;
@ -35,6 +34,7 @@ use crate::graph_util::graph_exit_integrity_errors;
use crate::npm::CliNpmResolver;
use crate::npm::ManagedCliNpmResolver;
use crate::util::checksum;
use crate::util::display::DisplayTreeNode;
const JSON_SCHEMA_VERSION: u8 = 1;
@ -342,76 +342,6 @@ fn add_npm_packages_to_json(
json.insert("npmPackages".to_string(), json_packages.into());
}
struct TreeNode {
text: String,
children: Vec<TreeNode>,
}
impl TreeNode {
pub fn from_text(text: String) -> Self {
Self {
text,
children: Default::default(),
}
}
}
fn print_tree_node<TWrite: Write>(
tree_node: &TreeNode,
writer: &mut TWrite,
) -> fmt::Result {
fn print_children<TWrite: Write>(
writer: &mut TWrite,
prefix: &str,
children: &[TreeNode],
) -> fmt::Result {
const SIBLING_CONNECTOR: char = '├';
const LAST_SIBLING_CONNECTOR: char = '└';
const CHILD_DEPS_CONNECTOR: char = '┬';
const CHILD_NO_DEPS_CONNECTOR: char = '─';
const VERTICAL_CONNECTOR: char = '│';
const EMPTY_CONNECTOR: char = ' ';
let child_len = children.len();
for (index, child) in children.iter().enumerate() {
let is_last = index + 1 == child_len;
let sibling_connector = if is_last {
LAST_SIBLING_CONNECTOR
} else {
SIBLING_CONNECTOR
};
let child_connector = if child.children.is_empty() {
CHILD_NO_DEPS_CONNECTOR
} else {
CHILD_DEPS_CONNECTOR
};
writeln!(
writer,
"{} {}",
colors::gray(format!("{prefix}{sibling_connector}{child_connector}")),
child.text
)?;
let child_prefix = format!(
"{}{}{}",
prefix,
if is_last {
EMPTY_CONNECTOR
} else {
VERTICAL_CONNECTOR
},
EMPTY_CONNECTOR
);
print_children(writer, &child_prefix, &child.children)?;
}
Ok(())
}
writeln!(writer, "{}", tree_node.text)?;
print_children(writer, "", &tree_node.children)?;
Ok(())
}
/// Precached information about npm packages that are used in deno info.
#[derive(Default)]
struct NpmInfo {
@ -568,7 +498,7 @@ impl<'a> GraphDisplayContext<'a> {
)?;
writeln!(writer)?;
let root_node = self.build_module_info(root, false);
print_tree_node(&root_node, writer)?;
root_node.print(writer)?;
Ok(())
}
Err(err) => {
@ -584,7 +514,7 @@ impl<'a> GraphDisplayContext<'a> {
}
}
fn build_dep_info(&mut self, dep: &Dependency) -> Vec<TreeNode> {
fn build_dep_info(&mut self, dep: &Dependency) -> Vec<DisplayTreeNode> {
let mut children = Vec::with_capacity(2);
if !dep.maybe_code.is_none() {
if let Some(child) = self.build_resolved_info(&dep.maybe_code, false) {
@ -599,7 +529,11 @@ impl<'a> GraphDisplayContext<'a> {
children
}
fn build_module_info(&mut self, module: &Module, type_dep: bool) -> TreeNode {
fn build_module_info(
&mut self,
module: &Module,
type_dep: bool,
) -> DisplayTreeNode {
enum PackageOrSpecifier {
Package(Box<NpmResolutionPackage>),
Specifier(ModuleSpecifier),
@ -645,7 +579,7 @@ impl<'a> GraphDisplayContext<'a> {
format!("{} {}", header_text, maybe_size_to_text(maybe_size))
};
let mut tree_node = TreeNode::from_text(header_text);
let mut tree_node = DisplayTreeNode::from_text(header_text);
if !was_seen {
match &package_or_specifier {
@ -683,14 +617,14 @@ impl<'a> GraphDisplayContext<'a> {
fn build_npm_deps(
&mut self,
package: &NpmResolutionPackage,
) -> Vec<TreeNode> {
) -> Vec<DisplayTreeNode> {
let mut deps = package.dependencies.values().collect::<Vec<_>>();
deps.sort();
let mut children = Vec::with_capacity(deps.len());
for dep_id in deps.into_iter() {
let maybe_size = self.npm_info.package_sizes.get(dep_id).cloned();
let size_str = maybe_size_to_text(maybe_size);
let mut child = TreeNode::from_text(format!(
let mut child = DisplayTreeNode::from_text(format!(
"npm:/{} {}",
dep_id.as_serialized(),
size_str
@ -715,7 +649,7 @@ impl<'a> GraphDisplayContext<'a> {
&mut self,
err: &ModuleError,
specifier: &ModuleSpecifier,
) -> TreeNode {
) -> DisplayTreeNode {
self.seen.insert(specifier.to_string());
match err {
ModuleError::InvalidTypeAssertion { .. } => {
@ -758,8 +692,8 @@ impl<'a> GraphDisplayContext<'a> {
&self,
specifier: &ModuleSpecifier,
error_msg: &str,
) -> TreeNode {
TreeNode::from_text(format!(
) -> DisplayTreeNode {
DisplayTreeNode::from_text(format!(
"{} {}",
colors::red(specifier),
colors::red_bold(error_msg)
@ -770,7 +704,7 @@ impl<'a> GraphDisplayContext<'a> {
&mut self,
resolution: &Resolution,
type_dep: bool,
) -> Option<TreeNode> {
) -> Option<DisplayTreeNode> {
match resolution {
Resolution::Ok(resolved) => {
let specifier = &resolved.specifier;
@ -778,14 +712,14 @@ impl<'a> GraphDisplayContext<'a> {
Some(match self.graph.try_get(resolved_specifier) {
Ok(Some(module)) => self.build_module_info(module, type_dep),
Err(err) => self.build_error_info(err, resolved_specifier),
Ok(None) => TreeNode::from_text(format!(
Ok(None) => DisplayTreeNode::from_text(format!(
"{} {}",
colors::red(specifier),
colors::red_bold("(missing)")
)),
})
}
Resolution::Err(err) => Some(TreeNode::from_text(format!(
Resolution::Err(err) => Some(DisplayTreeNode::from_text(format!(
"{} {}",
colors::italic(err.to_string()),
colors::red_bold("(resolve error)")

View file

@ -2,6 +2,7 @@
use deno_core::error::AnyError;
use deno_core::serde_json;
use deno_runtime::colors;
use std::io::Write;
/// A function that converts a float to a string the represents a human
@ -85,6 +86,78 @@ where
Ok(())
}
pub struct DisplayTreeNode {
pub text: String,
pub children: Vec<DisplayTreeNode>,
}
impl DisplayTreeNode {
pub fn from_text(text: String) -> Self {
Self {
text,
children: Default::default(),
}
}
pub fn print<TWrite: std::fmt::Write>(
&self,
writer: &mut TWrite,
) -> std::fmt::Result {
fn print_children<TWrite: std::fmt::Write>(
writer: &mut TWrite,
prefix: &str,
children: &[DisplayTreeNode],
) -> std::fmt::Result {
const SIBLING_CONNECTOR: char = '├';
const LAST_SIBLING_CONNECTOR: char = '└';
const CHILD_DEPS_CONNECTOR: char = '┬';
const CHILD_NO_DEPS_CONNECTOR: char = '─';
const VERTICAL_CONNECTOR: char = '│';
const EMPTY_CONNECTOR: char = ' ';
let child_len = children.len();
for (index, child) in children.iter().enumerate() {
let is_last = index + 1 == child_len;
let sibling_connector = if is_last {
LAST_SIBLING_CONNECTOR
} else {
SIBLING_CONNECTOR
};
let child_connector = if child.children.is_empty() {
CHILD_NO_DEPS_CONNECTOR
} else {
CHILD_DEPS_CONNECTOR
};
writeln!(
writer,
"{} {}",
colors::gray(format!(
"{prefix}{sibling_connector}─{child_connector}"
)),
child.text
)?;
let child_prefix = format!(
"{}{}{}",
prefix,
if is_last {
EMPTY_CONNECTOR
} else {
VERTICAL_CONNECTOR
},
EMPTY_CONNECTOR
);
print_children(writer, &child_prefix, &child.children)?;
}
Ok(())
}
writeln!(writer, "{}", self.text)?;
print_children(writer, "", &self.children)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -1119,6 +1119,11 @@ Compile file:///[WILDCARD]/main.ts to [WILDCARD]
Warning Failed resolving symlink. Ignoring.
Path: [WILDCARD]
Message: [WILDCARD])
Embedded File System
[WILDCARD]
"#,
);

View file

@ -2,3 +2,9 @@ Warning Parsing failed within the specified environment file: environment.env at
Check [WILDCARD]main.ts
Compile [WILDCARD]main.ts to out[WILDCARD]
Warning Environment variables from the file "environment.env" were embedded in the generated executable file
Embedded File System
out[WILDLINE]
└── main.ts

View file

@ -1,2 +1,6 @@
Check file:///[WILDLINE]/main.js
Compile file:///[WILDLINE]
Embedded File System
[WILDCARD]

View file

@ -1,2 +1,6 @@
Compile file:///[WILDCARD]/node_modules_symlink_outside/main.ts to [WILDCARD]
Warning Symlink target is outside '[WILDCARD]compile'. Inlining symlink at '[WILDCARD]node_modules_symlink_outside[WILDCARD]node_modules[WILDCARD]test.txt' to '[WILDCARD]target.txt' as file.
Warning Symlink target is outside '[WILDCARD]node_modules_symlink_outside'. Inlining symlink at '[WILDCARD]node_modules_symlink_outside[WILDCARD]node_modules[WILDCARD]test.txt' to '[WILDCARD]target.txt' as file.
Embedded File System
[WILDCARD]

View file

@ -3,4 +3,8 @@ Download http://localhost:4260/@denotest/esm-basic/1.0.0.tgz
Initialize @denotest/esm-basic@1.0.0
Check file:///[WILDCARD]/node_modules_symlink_outside/main.ts
Compile file:///[WILDCARD]/node_modules_symlink_outside/main.ts to [WILDLINE]
Warning Symlink target is outside '[WILDLINE]compile'. Excluding symlink at '[WILDLINE]node_modules_symlink_outside[WILDLINE]node_modules[WILDLINE]symlink_dir' with target '[WILDLINE]some_folder'.
Warning Symlink target is outside '[WILDLINE]node_modules_symlink_outside'. Excluding symlink at '[WILDLINE]node_modules_symlink_outside[WILDLINE]node_modules[WILDLINE]symlink_dir' with target '[WILDLINE]some_folder'.
Embedded File System
[WILDCARD]