11// Copyright 2018-2026 the Deno authors. MIT license.
22
33use std:: borrow:: Cow ;
4+ use std:: collections:: HashMap ;
45use std:: collections:: HashSet ;
6+ use std:: ffi:: OsString ;
7+ use std:: num:: NonZeroUsize ;
58use std:: path:: PathBuf ;
69use std:: rc:: Rc ;
710use std:: sync:: Arc ;
811
912use dashmap:: DashMap ;
1013use deno_core:: error:: AnyError ;
14+ use deno_core:: futures:: StreamExt ;
1115use deno_core:: serde_json;
1216use deno_core:: url:: Url ;
1317use deno_error:: JsErrorBox ;
@@ -27,6 +31,7 @@ use deno_npm_installer::lifecycle_scripts::LIFECYCLE_SCRIPTS_RUNNING_ENV_VAR;
2731use deno_npm_installer:: lifecycle_scripts:: LifecycleScriptsExecutor ;
2832use deno_npm_installer:: lifecycle_scripts:: LifecycleScriptsExecutorOptions ;
2933use deno_npm_installer:: lifecycle_scripts:: PackageWithScript ;
34+ use deno_npm_installer:: lifecycle_scripts:: compute_lifecycle_script_layers;
3035use deno_npm_installer:: lifecycle_scripts:: is_broken_default_install_script;
3136use deno_resolver:: npm:: ByonmNpmResolverCreateOptions ;
3237use deno_resolver:: npm:: ManagedNpmResolverRc ;
@@ -349,6 +354,11 @@ pub struct DenoTaskLifeCycleScriptsExecutor {
349354 system_info : deno_npm:: NpmSystemInfo ,
350355}
351356
357+ struct PackageScriptResult < ' a > {
358+ package : & ' a NpmResolutionPackage ,
359+ failed : Option < & ' a PackageNv > ,
360+ }
361+
352362#[ async_trait:: async_trait( ?Send ) ]
353363impl LifecycleScriptsExecutor for DenoTaskLifeCycleScriptsExecutor {
354364 async fn execute (
@@ -378,105 +388,48 @@ impl LifecycleScriptsExecutor for DenoTaskLifeCycleScriptsExecutor {
378388 // so the subprocess can detect that it is running as part of a lifecycle script,
379389 // and avoid trying to set up node_modules again
380390 env_vars. insert ( LIFECYCLE_SCRIPTS_RUNNING_ENV_VAR . into ( ) , "1" . into ( ) ) ;
381- // we want to pass the current state of npm resolution down to the deno subprocess
382- // (that may be running as part of the script). we do this with an inherited temp file
383- //
384- // SAFETY: we are sharing a single temp file across all of the scripts. the file position
385- // will be shared among these, which is okay since we run only one script at a time.
386- // However, if we concurrently run scripts in the future we will
387- // have to have multiple temp files.
388- let temp_file_fd = deno_runtime:: deno_process:: npm_process_state_tempfile (
389- options. process_state . as_bytes ( ) ,
390- )
391- . map_err ( DenoTaskLifecycleScriptsError :: CreateNpmProcessState ) ?;
392- // SAFETY: fd/handle is valid
393- let _temp_file = unsafe { std:: fs:: File :: from_raw_io_handle ( temp_file_fd) } ; // make sure the file gets closed
394- env_vars. insert (
395- deno_runtime:: deno_process:: NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME . into ( ) ,
396- ( temp_file_fd as usize ) . to_string ( ) . into ( ) ,
391+
392+ let concurrency = std:: thread:: available_parallelism ( )
393+ . ok ( )
394+ . and_then ( |n| NonZeroUsize :: new ( n. get ( ) . saturating_sub ( 1 ) ) )
395+ . unwrap_or_else ( || NonZeroUsize :: new ( 2 ) . unwrap ( ) )
396+ . get ( ) ;
397+
398+ let layers = compute_lifecycle_script_layers (
399+ options. packages_with_scripts ,
400+ options. snapshot ,
397401 ) ;
398- for PackageWithScript {
399- package,
400- scripts,
401- package_folder,
402- } in options. packages_with_scripts
403- {
404- // add custom commands for binaries from the package's dependencies. this will take precedence over the
405- // baseline commands, so if the package relies on a bin that conflicts with one higher in the dependency tree, the
406- // correct bin will be used.
407- let custom_commands = self
408- . resolve_custom_commands_from_deps (
409- options. extra_info_provider ,
410- base. clone ( ) ,
411- package,
412- options. snapshot ,
413- )
414- . await ;
415- for script_name in [ "preinstall" , "install" , "postinstall" ] {
416- if let Some ( script) = scripts. get ( script_name) {
417- if script_name == "install"
418- && is_broken_default_install_script ( & sys, script, package_folder)
419- {
420- continue ;
421- }
422- let _guard = self . progress_bar . update_with_prompt (
423- ProgressMessagePrompt :: Initialize ,
424- & format ! ( "{}: running '{script_name}' script" , package. id. nv) ,
425- ) ;
426- let crate :: task_runner:: TaskResult {
427- exit_code,
428- stderr,
429- stdout,
430- } =
431- crate :: task_runner:: run_task ( crate :: task_runner:: RunTaskOptions {
432- task_name : script_name,
433- script,
434- cwd : package_folder. clone ( ) ,
435- env_vars : env_vars. clone ( ) ,
436- custom_commands : custom_commands. clone ( ) ,
437- init_cwd : options. init_cwd ,
438- argv : & [ ] ,
439- root_node_modules_dir : Some ( options. root_node_modules_dir_path ) ,
440- stdio : Some ( crate :: task_runner:: TaskIo {
441- stderr : TaskStdio :: piped ( ) ,
442- stdout : TaskStdio :: piped ( ) ,
443- } ) ,
444- kill_signal : kill_signal. clone ( ) ,
445- } )
446- . await
447- . map_err ( DenoTaskLifecycleScriptsError :: Task ) ?;
448- let stdout = stdout. unwrap ( ) ;
449- let stderr = stderr. unwrap ( ) ;
450- if exit_code != 0 {
451- log:: warn!(
452- "error: script '{}' in '{}' failed with exit code {}{}{}" ,
453- script_name,
454- package. id. nv,
455- exit_code,
456- if !stdout. trim_ascii( ) . is_empty( ) {
457- format!(
458- "\n stdout:\n {}\n " ,
459- String :: from_utf8_lossy( & stdout) . trim( )
460- )
461- } else {
462- String :: new( )
463- } ,
464- if !stderr. trim_ascii( ) . is_empty( ) {
465- format!(
466- "\n stderr:\n {}\n " ,
467- String :: from_utf8_lossy( & stderr) . trim( )
468- )
469- } else {
470- String :: new( )
471- } ,
472- ) ;
473- failed_packages. push ( & package. id . nv ) ;
474- // assume if earlier script fails, later ones will fail too
475- break ;
476- }
402+
403+ for layer in & layers {
404+ log:: debug!(
405+ "Running lifecycle scripts layer: {}" ,
406+ layer
407+ . iter( )
408+ . map( |l| l. package. id. as_serialized( ) )
409+ . collect:: <Vec <_>>( )
410+ . join( ", " )
411+ ) ;
412+
413+ let mut results =
414+ deno_core:: futures:: stream:: iter ( layer. iter ( ) . map ( |pkg| {
415+ self . run_single_package_scripts (
416+ pkg,
417+ & env_vars,
418+ & base,
419+ & options,
420+ & kill_signal,
421+ & sys,
422+ )
423+ } ) )
424+ . buffer_unordered ( concurrency) ;
425+
426+ while let Some ( result) = results. next ( ) . await {
427+ let result = result?;
428+ if let Some ( nv) = result. failed {
429+ failed_packages. push ( nv) ;
477430 }
431+ ( options. on_ran_pkg_scripts ) ( result. package ) ?;
478432 }
479- ( options. on_ran_pkg_scripts ) ( package) ?;
480433 }
481434
482435 // re-set up bin entries for the packages which we've run scripts for.
@@ -524,6 +477,118 @@ impl DenoTaskLifeCycleScriptsExecutor {
524477 }
525478 }
526479
480+ /// Runs lifecycle scripts for a single package (preinstall, install,
481+ /// postinstall in order). Each package gets its own temp file for
482+ /// npm process state so concurrent execution is safe.
483+ async fn run_single_package_scripts < ' a > (
484+ & self ,
485+ pkg : & ' a PackageWithScript < ' a > ,
486+ env_vars : & HashMap < OsString , OsString > ,
487+ base_custom_commands : & crate :: task_runner:: TaskCustomCommands ,
488+ options : & LifecycleScriptsExecutorOptions < ' a > ,
489+ kill_signal : & KillSignal ,
490+ sys : & CliSys ,
491+ ) -> Result < PackageScriptResult < ' a > , AnyError > {
492+ let PackageWithScript {
493+ package,
494+ scripts,
495+ package_folder,
496+ } = pkg;
497+
498+ // each concurrent package gets its own temp file to avoid fd races
499+ let temp_file_fd = deno_runtime:: deno_process:: npm_process_state_tempfile (
500+ options. process_state . as_bytes ( ) ,
501+ )
502+ . map_err ( DenoTaskLifecycleScriptsError :: CreateNpmProcessState ) ?;
503+ // SAFETY: fd/handle is valid
504+ let _temp_file = unsafe { std:: fs:: File :: from_raw_io_handle ( temp_file_fd) } ;
505+ let mut env_vars = env_vars. clone ( ) ;
506+ env_vars. insert (
507+ deno_runtime:: deno_process:: NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME . into ( ) ,
508+ ( temp_file_fd as usize ) . to_string ( ) . into ( ) ,
509+ ) ;
510+
511+ // add custom commands for binaries from the package's dependencies.
512+ // this will take precedence over the baseline commands, so if the
513+ // package relies on a bin that conflicts with one higher in the
514+ // dependency tree, the correct bin will be used.
515+ let custom_commands = self
516+ . resolve_custom_commands_from_deps (
517+ options. extra_info_provider ,
518+ base_custom_commands. clone ( ) ,
519+ package,
520+ options. snapshot ,
521+ )
522+ . await ;
523+
524+ let mut failed = None ;
525+ for script_name in [ "preinstall" , "install" , "postinstall" ] {
526+ if let Some ( script) = scripts. get ( script_name) {
527+ if script_name == "install"
528+ && is_broken_default_install_script ( sys, script, package_folder)
529+ {
530+ continue ;
531+ }
532+ let _guard = self . progress_bar . update_with_prompt (
533+ ProgressMessagePrompt :: Initialize ,
534+ & format ! ( "{}: running '{script_name}' script" , package. id. nv) ,
535+ ) ;
536+ let crate :: task_runner:: TaskResult {
537+ exit_code,
538+ stderr,
539+ stdout,
540+ } = crate :: task_runner:: run_task ( crate :: task_runner:: RunTaskOptions {
541+ task_name : script_name,
542+ script,
543+ cwd : package_folder. clone ( ) ,
544+ env_vars : env_vars. clone ( ) ,
545+ custom_commands : custom_commands. clone ( ) ,
546+ init_cwd : options. init_cwd ,
547+ argv : & [ ] ,
548+ root_node_modules_dir : Some ( options. root_node_modules_dir_path ) ,
549+ stdio : Some ( crate :: task_runner:: TaskIo {
550+ stderr : TaskStdio :: piped ( ) ,
551+ stdout : TaskStdio :: piped ( ) ,
552+ } ) ,
553+ kill_signal : kill_signal. clone ( ) ,
554+ } )
555+ . await
556+ . map_err ( DenoTaskLifecycleScriptsError :: Task ) ?;
557+ let stdout = stdout. unwrap ( ) ;
558+ let stderr = stderr. unwrap ( ) ;
559+ if exit_code != 0 {
560+ log:: warn!(
561+ "error: script '{}' in '{}' failed with exit code {}{}{}" ,
562+ script_name,
563+ package. id. nv,
564+ exit_code,
565+ if !stdout. trim_ascii( ) . is_empty( ) {
566+ format!(
567+ "\n stdout:\n {}\n " ,
568+ String :: from_utf8_lossy( & stdout) . trim( )
569+ )
570+ } else {
571+ String :: new( )
572+ } ,
573+ if !stderr. trim_ascii( ) . is_empty( ) {
574+ format!(
575+ "\n stderr:\n {}\n " ,
576+ String :: from_utf8_lossy( & stderr) . trim( )
577+ )
578+ } else {
579+ String :: new( )
580+ } ,
581+ ) ;
582+ failed = Some ( & package. id . nv ) ;
583+ // assume if earlier script fails, later ones will fail too
584+ break ;
585+ }
586+ }
587+ }
588+
589+ Ok ( PackageScriptResult { package, failed } )
590+ }
591+
527592 // take in all (non copy) packages from snapshot,
528593 // and resolve the set of available binaries to create
529594 // custom commands available to the task runner
0 commit comments