Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

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

2 changes: 2 additions & 0 deletions src/bundler/BundleThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ impl<C: CompletionStruct> BundleThread<C> {
let ptr = SendPtr(instance);
let thread = std::thread::Builder::new()
.name("Bundler".into())
// `Bun.build({ compile })` writes its executable here; use bun's usual stack.
.stack_size(bun_threading::thread_pool::DEFAULT_THREAD_STACK_SIZE as usize)
.spawn(move || {
let ptr = ptr;
// SAFETY: caller guarantees `instance` is valid for 'static; `thread_main`
Expand Down
36 changes: 34 additions & 2 deletions src/options_types/compile_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,40 @@ pub enum ParseError {
InvalidTarget,
}

/// Env-derived settings for downloading a target's executable; see [`CompileTarget::download_options`].
pub struct DownloadOptions {
pub reject_unauthorized: bool,
/// Proxy for the registry URL, with `NO_PROXY` already applied.
pub http_proxy: Option<Box<[u8]>>,
}

impl Default for DownloadOptions {
/// Placeholder until [`CompileTarget::download_options`] fills it in; verifies TLS.
fn default() -> Self {
Self {
reject_unauthorized: true,
http_proxy: None,
}
}
}

impl CompileTarget {
/// Buffer size callers hand to [`Self::to_npm_registry_url`].
pub const REGISTRY_URL_BUF_LEN: usize = 2048;

pub fn download_options(&self, env: &bun_dotenv::Loader) -> DownloadOptions {
let mut url_buf = [0u8; Self::REGISTRY_URL_BUF_LEN];
// A URL that cannot be built fails the download itself, which reports it.
let http_proxy = self.to_npm_registry_url(&mut url_buf).ok().and_then(|url| {
env.get_http_proxy_for(&bun_url::URL::parse(url))
.map(|proxy| Box::from(proxy.href))
});
DownloadOptions {
reject_unauthorized: env.get_tls_reject_unauthorized(),
http_proxy,
}
}

pub(crate) fn eql(&self, other: &CompileTarget) -> bool {
self.os == other.os
&& self.arch == other.arch
Expand Down Expand Up @@ -185,7 +218,6 @@ impl CompileTarget {
&self,
buf: &'a mut PathBuffer,
version_str: &'a ZStr,
_env: &mut bun_dotenv::Loader,
needs_download: &mut bool,
) -> &'a ZStr {
if self.is_default() {
Expand All @@ -206,7 +238,7 @@ impl CompileTarget {
return version_str;
}

// T1 fallback ignores `_env` (full env-override chain lives in bun_install).
// The full cache-dir override chain lives in bun_install; this is its fallback.
let cache_dir = bun_sys::fetch_cache_directory_path();
let dest = path::resolve_path::join_abs_string_buf_z::<path::platform::Auto>(
path::fs::FileSystem::instance().top_level_dir(),
Expand Down
8 changes: 7 additions & 1 deletion src/runtime/api/JSBundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use bun_core::Output;
use bun_core::{String as BunString, ZigString};
use bun_jsc::ConcurrentTask::ConcurrentTask;
use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult};
use bun_options_types::compile_target::CompileTarget;
use bun_options_types::compile_target::{CompileTarget, DownloadOptions};
use bun_options_types::schema::api; // bun.schema.api
use bun_standalone_graph::StandaloneModuleGraph;

Expand Down Expand Up @@ -223,6 +223,8 @@ pub mod js_bundler {

pub struct CompileOptions {
pub(crate) compile_target: CompileTarget,
/// Taken from the VM's env on the calling thread; the download runs on the bundle thread.
pub(crate) download: DownloadOptions,
pub(crate) exec_argv: OwnedString,
pub(crate) executable_path: OwnedString,
pub(crate) windows_hide_console: bool,
Expand All @@ -244,6 +246,7 @@ pub mod js_bundler {
fn default() -> Self {
Self {
compile_target: CompileTarget::default(),
download: DownloadOptions::default(),
exec_argv: OwnedString::default(),
executable_path: OwnedString::default(),
windows_hide_console: false,
Expand Down Expand Up @@ -1197,6 +1200,9 @@ pub mod js_bundler {
let is_standalone_html = this.target == Target::Browser && has_all_html_entrypoints;
if !is_standalone_html {
this.target = Target::Bun;
compile.download = compile
.compile_target
.download_options(global_this.bun_vm().env_loader());

let define_keys = compile.compile_target.define_keys();
let define_values = compile.compile_target.define_values();
Expand Down
76 changes: 35 additions & 41 deletions src/runtime/api/js_bundle_completion_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ impl JSBundleCompletionTask {
Ok(())
}

/// Port of `JSBundleCompletionTask.doCompilation`.
fn do_compilation(&mut self, output_files: &mut Vec<OutputFile>) -> CompileResult {
/// Port of `JSBundleCompletionTask.doCompilation`. Bundle thread: nothing in here may touch JS.
fn do_compilation(&self, output_files: &mut Vec<OutputFile>) -> CompileResult {
let compile_options = self
.config
.compile
Expand Down Expand Up @@ -426,10 +426,7 @@ impl JSBundleCompletionTask {
root_dir.fd,
module_prefix,
outfile_for_executable,
// SAFETY: `self.env` is the per-VM `DotEnv.Loader` stashed at
// construction; valid for the lifetime of the VirtualMachine, and
// nothing inside `to_executable` reaches it otherwise.
unsafe { &mut *self.env },
&compile_options.download,
self.config.format,
&WindowsOptions {
hide_console: compile_options.windows_hide_console,
Expand Down Expand Up @@ -559,6 +556,36 @@ impl JSBundleCompletionTask {
result
}

/// `Bun.build({ compile })`: the bundle becomes the executable before `on_complete` gets it.
fn compile_on_bundle_thread(&mut self) {
if self.config.compile.is_none()
|| self.cancelled.load(core::sync::atomic::Ordering::Acquire)
{
return;
}
let mut build = match core::mem::replace(&mut self.result, BundleV2Result::Pending) {
BundleV2Result::Value(build) => build,
not_bundled => {
self.result = not_bundled;
return;
}
};
let compile_result = self.do_compilation(&mut build.output_files);
// Nothing else flushes this thread's buffered stderr, which the above reports failures to.
bun_core::Output::flush();
self.result = match compile_result {
CompileResult::Success => BundleV2Result::Value(build),
CompileResult::Err(err) => {
self.log.add_error_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!("{}", bstr::BStr::new(err.slice())),
);
BundleV2Result::Err(bun_bundler::Error::CompilationFailed)
}
};
}

pub(crate) fn on_complete_anytask(ctx: *mut Self) -> bun_event_loop::JsResult<()> {
crate::jsc_hooks::ActiveHandle::Bundle(NonNull::new(ctx).expect("completion")).unregister();
// For the +1 taken by `complete_on_bundle_thread` enqueue.
Expand Down Expand Up @@ -644,7 +671,7 @@ impl JSBundleCompletionTask {
}

// Copy the BackRef out (it is `Copy`) so `global_this` borrows a local
// instead of `*this` — `do_compilation`/`to_js_error` below need `&mut *this`.
// instead of `*this` — `to_js_error` below needs `&mut *this`.
let global_this_ref = this.global_this;
let global_this = global_this_ref.get();
// `Strong::swap` ties the returned `&mut JSPromise` to
Expand All @@ -654,40 +681,6 @@ impl JSBundleCompletionTask {
let promise: *mut JSPromise = this.promise.swap();
let promise = JSPromise::opaque_mut(promise);

// `do_compilation` borrows `&mut self` while needing
// `&mut output_files` from inside `self.result`. Temporarily move the
// Vec out via `take` so the method gets a disjoint `&mut self`.
if matches!(this.result, BundleV2Result::Value(_)) && this.config.compile.is_some() {
let mut output_files = match &mut this.result {
BundleV2Result::Value(build) => core::mem::take(&mut build.output_files),
// SAFETY: arm checked above.
_ => unsafe { core::hint::unreachable_unchecked() },
};
let compile_result = this.do_compilation(&mut output_files);
// `defer compile_result.deinit()` — `CompileResult` is a Rust enum
// with owned `Vec<u8>` payloads; drops at end of scope.

if let CompileResult::Err(err) = &compile_result {
// `bun.handleOom(log.addError(..., bun.handleOom(dupe(..))))`
this.log.add_error_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!("{}", bstr::BStr::new(err.slice())),
);
// `this.result.value.deinit()` — owned fields drop with the
// overwrite below; `output_files` (moved out above) drops here.
drop(output_files);
this.result = BundleV2Result::Err(bun_bundler::Error::CompilationFailed);
} else {
// Put the compacted output_files back.
match &mut this.result {
BundleV2Result::Value(build) => build.output_files = output_files,
// SAFETY: arm checked above.
_ => unsafe { core::hint::unreachable_unchecked() },
}
}
}

// `to_js_error` borrows `&mut self`, which would overlap a
// `&mut this.result` match scrutinee. Dispatch the pending/err arms
// first, then take a fresh `&mut` for Value.
Expand Down Expand Up @@ -1115,6 +1108,7 @@ impl CompletionStruct for JSBundleCompletionTask {
}

fn complete_on_bundle_thread(&mut self) {
self.compile_on_bundle_thread();
// The bundle thread's last touch of this task and of the VM's memory:
// hand it back (always queued — the VM waits for it) and stop counting.
self.bundle_loop
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/cli/build_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ impl BuildCommand {
&opt_public_path,
outfile,
// SAFETY: `env` is a process-lifetime singleton.
unsafe { &mut *env_ptr },
&compile_target.download_options(unsafe { &*env_ptr }),
opt_output_format,
&ctx.bundler_options.windows,
ctx.bundler_options
Expand Down
1 change: 0 additions & 1 deletion src/standalone_graph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ bun_alloc.workspace = true
bun_core.workspace = true
bun_bundler.workspace = true
bun_collections.workspace = true
bun_dotenv.workspace = true
bun_exe_format.workspace = true
bun_http.workspace = true
bun_parsers.workspace = true
Expand Down
20 changes: 8 additions & 12 deletions src/standalone_graph/StandaloneModuleGraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1646,15 +1646,15 @@ pub(crate) fn inject(
}

use bun_core::Environment::OperatingSystem as CompileTargetOs;
pub use bun_options_types::compile_target::CompileTarget;
pub use bun_options_types::compile_target::{CompileTarget, DownloadOptions};

/// Moved up from `bun_options_types` (T3) so it can name
/// `bun_http::AsyncHTTP` directly
/// instead of routing through `extern "Rust"` shims; the only callers are the
/// two `download*` fns below in this crate.
pub(crate) fn download_to_path(
target: &CompileTarget,
env: &mut bun_dotenv::Loader,
download: &DownloadOptions,
dest_z: &ZStr,
Comment thread
claude[bot] marked this conversation as resolved.
) -> crate::Result<()> {
bun_http::http_thread::init(&Default::default());
Expand All @@ -1666,7 +1666,7 @@ pub(crate) fn download_to_path(
// TODO: This is way too much code necessary to send a single HTTP request...
let mut compressed_archive_bytes =
Box::new(bun_core::MutableString::init(24 * 1024 * 1024)?);
let mut url_buffer = [0u8; 2048];
let mut url_buffer = [0u8; CompileTarget::REGISTRY_URL_BUF_LEN];
let url_str = match target.to_npm_registry_url(&mut url_buffer) {
Ok(s) => s,
Err(err) => {
Expand All @@ -1681,10 +1681,6 @@ pub(crate) fn download_to_path(
// `progress.end()` below is sufficient: no fallible call sits between
// `refresher.start` and it, so every exit path (including the
// error returns after it) ends the node exactly once.
// Note: reshaped for borrowck — `get_http_proxy_for` borrows
// `env` for the proxy URL lifetime; read the bool first.
let reject_unauthorized = env.get_tls_reject_unauthorized();
let http_proxy: Option<bun_url::URL<'_>> = env.get_http_proxy_for(&url);
let progress = refresher.start(b"Downloading", 0);

let mut async_http = Box::new(bun_http::AsyncHTTP::init_sync(
Expand All @@ -1693,13 +1689,13 @@ pub(crate) fn download_to_path(
Default::default(),
b"",
b"",
http_proxy,
download.http_proxy.as_deref().map(bun_url::URL::parse),
None,
bun_http::FetchRedirect::Follow,
));
async_http.client.progress_node =
core::ptr::NonNull::new(core::ptr::from_mut(progress));
async_http.client.flags.reject_unauthorized = reject_unauthorized;
async_http.client.flags.reject_unauthorized = download.reject_unauthorized;
let send_result = async_http.send_sync(&mut compressed_archive_bytes);

progress.end();
Expand Down Expand Up @@ -1818,7 +1814,7 @@ pub fn to_executable(
root_dir: Fd,
module_prefix: &[u8],
outfile: &[u8],
env: &mut bun_dotenv::Loader,
download: &DownloadOptions,
output_format: Format,
windows_options: &WindowsOptions,
compile_exec_argv: &[u8],
Expand Down Expand Up @@ -1870,10 +1866,10 @@ pub fn to_executable(
let version_zstr = ZStr::from_slice_with_nul(&version_str[..]);

let mut needs_download: bool = true;
let dest_z = target.exe_path(&mut exe_path_buf, version_zstr, env, &mut needs_download);
let dest_z = target.exe_path(&mut exe_path_buf, version_zstr, &mut needs_download);

if needs_download {
if let Err(e) = download_to_path(target, env, dest_z) {
if let Err(e) = download_to_path(target, download, dest_z) {
return Ok(match e {
crate::Error::TargetNotFound => CompileResult::fail_fmt(format_args!(
"Target platform '{}' is not available for download. Check if this version of Bun supports this target.",
Expand Down
Loading
Loading