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 ;
@@ -28,6 +32,7 @@ use deno_npm_installer::lifecycle_scripts::LIFECYCLE_SCRIPTS_RUNNING_ENV_VAR;
2832use deno_npm_installer:: lifecycle_scripts:: LifecycleScriptsExecutor ;
2933use deno_npm_installer:: lifecycle_scripts:: LifecycleScriptsExecutorOptions ;
3034use deno_npm_installer:: lifecycle_scripts:: PackageWithScript ;
35+ use deno_npm_installer:: lifecycle_scripts:: compute_lifecycle_script_layers;
3136use deno_npm_installer:: lifecycle_scripts:: is_broken_default_install_script;
3237use deno_resolver:: npm:: ByonmNpmResolverCreateOptions ;
3338use deno_resolver:: npm:: ManagedNpmResolverRc ;
@@ -367,6 +372,11 @@ pub struct DenoTaskLifeCycleScriptsExecutor {
367372 system_info : deno_npm:: NpmSystemInfo ,
368373}
369374
375+ struct PackageScriptResult < ' a > {
376+ package : & ' a NpmResolutionPackage ,
377+ failed : Option < & ' a PackageNv > ,
378+ }
379+
370380#[ async_trait:: async_trait( ?Send ) ]
371381impl LifecycleScriptsExecutor for DenoTaskLifeCycleScriptsExecutor {
372382 async fn execute (
@@ -396,105 +406,48 @@ impl LifecycleScriptsExecutor for DenoTaskLifeCycleScriptsExecutor {
396406 // so the subprocess can detect that it is running as part of a lifecycle script,
397407 // and avoid trying to set up node_modules again
398408 env_vars. insert ( LIFECYCLE_SCRIPTS_RUNNING_ENV_VAR . into ( ) , "1" . into ( ) ) ;
399- // we want to pass the current state of npm resolution down to the deno subprocess
400- // (that may be running as part of the script). we do this with an inherited temp file
401- //
402- // SAFETY: we are sharing a single temp file across all of the scripts. the file position
403- // will be shared among these, which is okay since we run only one script at a time.
404- // However, if we concurrently run scripts in the future we will
405- // have to have multiple temp files.
406- let temp_file_fd = deno_runtime:: deno_process:: npm_process_state_tempfile (
407- options. process_state . as_bytes ( ) ,
408- )
409- . map_err ( DenoTaskLifecycleScriptsError :: CreateNpmProcessState ) ?;
410- // SAFETY: fd/handle is valid
411- let _temp_file = unsafe { std:: fs:: File :: from_raw_io_handle ( temp_file_fd) } ; // make sure the file gets closed
412- env_vars. insert (
413- deno_runtime:: deno_process:: NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME . into ( ) ,
414- ( temp_file_fd as usize ) . to_string ( ) . into ( ) ,
409+
410+ let concurrency = std:: thread:: available_parallelism ( )
411+ . ok ( )
412+ . and_then ( |n| NonZeroUsize :: new ( n. get ( ) . saturating_sub ( 1 ) ) )
413+ . unwrap_or_else ( || NonZeroUsize :: new ( 2 ) . unwrap ( ) )
414+ . get ( ) ;
415+
416+ let layers = compute_lifecycle_script_layers (
417+ options. packages_with_scripts ,
418+ options. snapshot ,
415419 ) ;
416- for PackageWithScript {
417- package,
418- scripts,
419- package_folder,
420- } in options. packages_with_scripts
421- {
422- // add custom commands for binaries from the package's dependencies. this will take precedence over the
423- // baseline commands, so if the package relies on a bin that conflicts with one higher in the dependency tree, the
424- // correct bin will be used.
425- let custom_commands = self
426- . resolve_custom_commands_from_deps (
427- options. extra_info_provider ,
428- base. clone ( ) ,
429- package,
430- options. snapshot ,
431- )
432- . await ;
433- for script_name in [ "preinstall" , "install" , "postinstall" ] {
434- if let Some ( script) = scripts. get ( script_name) {
435- if script_name == "install"
436- && is_broken_default_install_script ( & sys, script, package_folder)
437- {
438- continue ;
439- }
440- let _guard = self . progress_bar . update_with_prompt (
441- ProgressMessagePrompt :: Initialize ,
442- & format ! ( "{}: running '{script_name}' script" , package. id. nv) ,
443- ) ;
444- let crate :: task_runner:: TaskResult {
445- exit_code,
446- stderr,
447- stdout,
448- } =
449- crate :: task_runner:: run_task ( crate :: task_runner:: RunTaskOptions {
450- task_name : script_name,
451- script,
452- cwd : package_folder. clone ( ) ,
453- env_vars : env_vars. clone ( ) ,
454- custom_commands : custom_commands. clone ( ) ,
455- init_cwd : options. init_cwd ,
456- argv : & [ ] ,
457- root_node_modules_dir : Some ( options. root_node_modules_dir_path ) ,
458- stdio : Some ( crate :: task_runner:: TaskIo {
459- stderr : TaskStdio :: piped ( ) ,
460- stdout : TaskStdio :: piped ( ) ,
461- } ) ,
462- kill_signal : kill_signal. clone ( ) ,
463- } )
464- . await
465- . map_err ( DenoTaskLifecycleScriptsError :: Task ) ?;
466- let stdout = stdout. unwrap ( ) ;
467- let stderr = stderr. unwrap ( ) ;
468- if exit_code != 0 {
469- log:: warn!(
470- "error: script '{}' in '{}' failed with exit code {}{}{}" ,
471- script_name,
472- package. id. nv,
473- exit_code,
474- if !stdout. trim_ascii( ) . is_empty( ) {
475- format!(
476- "\n stdout:\n {}\n " ,
477- String :: from_utf8_lossy( & stdout) . trim( )
478- )
479- } else {
480- String :: new( )
481- } ,
482- if !stderr. trim_ascii( ) . is_empty( ) {
483- format!(
484- "\n stderr:\n {}\n " ,
485- String :: from_utf8_lossy( & stderr) . trim( )
486- )
487- } else {
488- String :: new( )
489- } ,
490- ) ;
491- failed_packages. push ( & package. id . nv ) ;
492- // assume if earlier script fails, later ones will fail too
493- break ;
494- }
420+
421+ for layer in & layers {
422+ log:: debug!(
423+ "Running lifecycle scripts layer: {}" ,
424+ layer
425+ . iter( )
426+ . map( |l| l. package. id. as_serialized( ) )
427+ . collect:: <Vec <_>>( )
428+ . join( ", " )
429+ ) ;
430+
431+ let mut results =
432+ deno_core:: futures:: stream:: iter ( layer. iter ( ) . map ( |pkg| {
433+ self . run_single_package_scripts (
434+ pkg,
435+ & env_vars,
436+ & base,
437+ & options,
438+ & kill_signal,
439+ & sys,
440+ )
441+ } ) )
442+ . buffer_unordered ( concurrency) ;
443+
444+ while let Some ( result) = results. next ( ) . await {
445+ let result = result?;
446+ if let Some ( nv) = result. failed {
447+ failed_packages. push ( nv) ;
495448 }
449+ ( options. on_ran_pkg_scripts ) ( result. package ) ?;
496450 }
497- ( options. on_ran_pkg_scripts ) ( package) ?;
498451 }
499452
500453 // re-set up bin entries for the packages which we've run scripts for.
@@ -542,6 +495,118 @@ impl DenoTaskLifeCycleScriptsExecutor {
542495 }
543496 }
544497
498+ /// Runs lifecycle scripts for a single package (preinstall, install,
499+ /// postinstall in order). Each package gets its own temp file for
500+ /// npm process state so concurrent execution is safe.
501+ async fn run_single_package_scripts < ' a > (
502+ & self ,
503+ pkg : & ' a PackageWithScript < ' a > ,
504+ env_vars : & HashMap < OsString , OsString > ,
505+ base_custom_commands : & crate :: task_runner:: TaskCustomCommands ,
506+ options : & LifecycleScriptsExecutorOptions < ' a > ,
507+ kill_signal : & KillSignal ,
508+ sys : & CliSys ,
509+ ) -> Result < PackageScriptResult < ' a > , AnyError > {
510+ let PackageWithScript {
511+ package,
512+ scripts,
513+ package_folder,
514+ } = pkg;
515+
516+ // each concurrent package gets its own temp file to avoid fd races
517+ let temp_file_fd = deno_runtime:: deno_process:: npm_process_state_tempfile (
518+ options. process_state . as_bytes ( ) ,
519+ )
520+ . map_err ( DenoTaskLifecycleScriptsError :: CreateNpmProcessState ) ?;
521+ // SAFETY: fd/handle is valid
522+ let _temp_file = unsafe { std:: fs:: File :: from_raw_io_handle ( temp_file_fd) } ;
523+ let mut env_vars = env_vars. clone ( ) ;
524+ env_vars. insert (
525+ deno_runtime:: deno_process:: NPM_RESOLUTION_STATE_FD_ENV_VAR_NAME . into ( ) ,
526+ ( temp_file_fd as usize ) . to_string ( ) . into ( ) ,
527+ ) ;
528+
529+ // add custom commands for binaries from the package's dependencies.
530+ // this will take precedence over the baseline commands, so if the
531+ // package relies on a bin that conflicts with one higher in the
532+ // dependency tree, the correct bin will be used.
533+ let custom_commands = self
534+ . resolve_custom_commands_from_deps (
535+ options. extra_info_provider ,
536+ base_custom_commands. clone ( ) ,
537+ package,
538+ options. snapshot ,
539+ )
540+ . await ;
541+
542+ let mut failed = None ;
543+ for script_name in [ "preinstall" , "install" , "postinstall" ] {
544+ if let Some ( script) = scripts. get ( script_name) {
545+ if script_name == "install"
546+ && is_broken_default_install_script ( sys, script, package_folder)
547+ {
548+ continue ;
549+ }
550+ let _guard = self . progress_bar . update_with_prompt (
551+ ProgressMessagePrompt :: Initialize ,
552+ & format ! ( "{}: running '{script_name}' script" , package. id. nv) ,
553+ ) ;
554+ let crate :: task_runner:: TaskResult {
555+ exit_code,
556+ stderr,
557+ stdout,
558+ } = crate :: task_runner:: run_task ( crate :: task_runner:: RunTaskOptions {
559+ task_name : script_name,
560+ script,
561+ cwd : package_folder. clone ( ) ,
562+ env_vars : env_vars. clone ( ) ,
563+ custom_commands : custom_commands. clone ( ) ,
564+ init_cwd : options. init_cwd ,
565+ argv : & [ ] ,
566+ root_node_modules_dir : Some ( options. root_node_modules_dir_path ) ,
567+ stdio : Some ( crate :: task_runner:: TaskIo {
568+ stderr : TaskStdio :: piped ( ) ,
569+ stdout : TaskStdio :: piped ( ) ,
570+ } ) ,
571+ kill_signal : kill_signal. clone ( ) ,
572+ } )
573+ . await
574+ . map_err ( DenoTaskLifecycleScriptsError :: Task ) ?;
575+ let stdout = stdout. unwrap ( ) ;
576+ let stderr = stderr. unwrap ( ) ;
577+ if exit_code != 0 {
578+ log:: warn!(
579+ "error: script '{}' in '{}' failed with exit code {}{}{}" ,
580+ script_name,
581+ package. id. nv,
582+ exit_code,
583+ if !stdout. trim_ascii( ) . is_empty( ) {
584+ format!(
585+ "\n stdout:\n {}\n " ,
586+ String :: from_utf8_lossy( & stdout) . trim( )
587+ )
588+ } else {
589+ String :: new( )
590+ } ,
591+ if !stderr. trim_ascii( ) . is_empty( ) {
592+ format!(
593+ "\n stderr:\n {}\n " ,
594+ String :: from_utf8_lossy( & stderr) . trim( )
595+ )
596+ } else {
597+ String :: new( )
598+ } ,
599+ ) ;
600+ failed = Some ( & package. id . nv ) ;
601+ // assume if earlier script fails, later ones will fail too
602+ break ;
603+ }
604+ }
605+ }
606+
607+ Ok ( PackageScriptResult { package, failed } )
608+ }
609+
545610 // take in all (non copy) packages from snapshot,
546611 // and resolve the set of available binaries to create
547612 // custom commands available to the task runner
0 commit comments