@@ -2398,6 +2398,7 @@ fn install_dep(dep: &ResolvedDep, modules_dir: &Path, install_mode: InstallMode)
23982398 safety:: validate_dependency_name ( & dep. name , "module dependency" ) ?;
23992399 let repo_path = git:: clone_or_fetch ( & dep. git ) ?;
24002400 let dest = modules_dir. join ( & dep. name ) ;
2401+ let dist_info_dest = modules_dir. join ( dist_info_dir_name ( dep) ) ;
24012402 let unique = std:: time:: SystemTime :: now ( )
24022403 . duration_since ( std:: time:: UNIX_EPOCH )
24032404 . map ( |d| d. as_nanos ( ) )
@@ -2408,7 +2409,15 @@ fn install_dep(dep: &ResolvedDep, modules_dir: &Path, install_mode: InstallMode)
24082409 }
24092410
24102411 git:: export_to ( & repo_path, & dep. rev , & staging) ?;
2411- materialize_module_dir ( & staging, & dest, install_mode) ?;
2412+ let module_subdir = select_module_subdir ( & staging, & dep. name ) ?;
2413+ let module_src = if module_subdir. as_os_str ( ) . is_empty ( ) {
2414+ staging. clone ( )
2415+ } else {
2416+ staging. join ( & module_subdir)
2417+ } ;
2418+
2419+ materialize_module_dir ( & module_src, & dest, install_mode) ?;
2420+ write_module_dist_info ( & staging, & module_subdir, & dist_info_dest) ?;
24122421 std:: fs:: remove_dir_all ( & staging) ?;
24132422 discover_module_use_path ( & dest, & dep. name )
24142423}
@@ -2520,7 +2529,135 @@ fn copy_dir(src: &Path, dest: &Path) -> Result<()> {
25202529 Ok ( ( ) )
25212530}
25222531
2532+ fn dist_info_dir_name ( dep : & ResolvedDep ) -> String {
2533+ let raw_version = dep
2534+ . tag
2535+ . as_deref ( )
2536+ . map ( str:: trim)
2537+ . filter ( |tag| !tag. is_empty ( ) )
2538+ . unwrap_or ( & dep. rev [ ..12 . min ( dep. rev . len ( ) ) ] ) ;
2539+ let version = sanitize_dist_info_version ( raw_version) ;
2540+ format ! ( "{}-{version}.dist-info" , dep. name)
2541+ }
2542+
2543+ fn sanitize_dist_info_version ( version : & str ) -> String {
2544+ let mut out = String :: with_capacity ( version. len ( ) ) ;
2545+ for ch in version. chars ( ) {
2546+ if ch. is_ascii_alphanumeric ( ) || matches ! ( ch, '.' | '_' | '-' ) {
2547+ out. push ( ch) ;
2548+ } else {
2549+ out. push ( '_' ) ;
2550+ }
2551+ }
2552+
2553+ if out. is_empty ( ) {
2554+ "unknown" . to_string ( )
2555+ } else {
2556+ out
2557+ }
2558+ }
2559+
2560+ fn write_module_dist_info (
2561+ repo_root : & Path ,
2562+ module_subdir : & Path ,
2563+ dist_info_dir : & Path ,
2564+ ) -> Result < ( ) > {
2565+ if dist_info_dir. exists ( ) {
2566+ std:: fs:: remove_dir_all ( dist_info_dir) ?;
2567+ }
2568+ std:: fs:: create_dir_all ( dist_info_dir) ?;
2569+
2570+ if module_subdir. as_os_str ( ) . is_empty ( ) {
2571+ copy_root_peripheral_entries ( repo_root, dist_info_dir) ?;
2572+ } else {
2573+ copy_repo_except_subdir ( repo_root, dist_info_dir, module_subdir) ?;
2574+ }
2575+
2576+ Ok ( ( ) )
2577+ }
2578+
2579+ fn copy_repo_except_subdir ( src_root : & Path , dest_root : & Path , skip_subdir : & Path ) -> Result < ( ) > {
2580+ for entry in WalkDir :: new ( src_root)
2581+ . follow_links ( false )
2582+ . into_iter ( )
2583+ . filter_map ( std:: result:: Result :: ok)
2584+ {
2585+ let relative = entry
2586+ . path ( )
2587+ . strip_prefix ( src_root)
2588+ . map_err ( |e| crate :: error:: QuiverError :: Other ( e. to_string ( ) ) ) ?;
2589+ if relative. as_os_str ( ) . is_empty ( ) || relative. starts_with ( skip_subdir) {
2590+ continue ;
2591+ }
2592+
2593+ let target = dest_root. join ( relative) ;
2594+ if entry. file_type ( ) . is_dir ( ) {
2595+ std:: fs:: create_dir_all ( & target) ?;
2596+ } else if entry. file_type ( ) . is_file ( ) {
2597+ if let Some ( parent) = target. parent ( ) {
2598+ std:: fs:: create_dir_all ( parent) ?;
2599+ }
2600+ std:: fs:: copy ( entry. path ( ) , & target) ?;
2601+ }
2602+ }
2603+
2604+ Ok ( ( ) )
2605+ }
2606+
2607+ fn copy_root_peripheral_entries ( repo_root : & Path , dist_info_dir : & Path ) -> Result < ( ) > {
2608+ for entry in std:: fs:: read_dir ( repo_root) ? {
2609+ let entry = entry?;
2610+ let name = entry. file_name ( ) . to_string_lossy ( ) . to_lowercase ( ) ;
2611+ let path = entry. path ( ) ;
2612+ if path. is_file ( ) {
2613+ if !should_include_root_peripheral_file ( & name) {
2614+ continue ;
2615+ }
2616+ std:: fs:: copy ( & path, dist_info_dir. join ( entry. file_name ( ) ) ) ?;
2617+ } else if path. is_dir ( ) {
2618+ if !should_include_root_peripheral_dir ( & name) {
2619+ continue ;
2620+ }
2621+ copy_dir ( & path, & dist_info_dir. join ( entry. file_name ( ) ) ) ?;
2622+ }
2623+ }
2624+
2625+ Ok ( ( ) )
2626+ }
2627+
2628+ fn should_include_root_peripheral_file ( name : & str ) -> bool {
2629+ let base = name. split ( '.' ) . next ( ) . unwrap_or ( name) ;
2630+ matches ! (
2631+ base,
2632+ "readme"
2633+ | "license"
2634+ | "licenses"
2635+ | "copying"
2636+ | "notice"
2637+ | "notices"
2638+ | "changelog"
2639+ | "changes"
2640+ | "contributing"
2641+ | "authors"
2642+ | "security"
2643+ | "code_of_conduct"
2644+ | "nupackage"
2645+ | "nupm"
2646+ ) || name. ends_with ( ".md" )
2647+ || name. ends_with ( ".rst" )
2648+ || name. ends_with ( ".txt" )
2649+ }
2650+
2651+ fn should_include_root_peripheral_dir ( name : & str ) -> bool {
2652+ matches ! ( name, "docs" | "doc" | ".github" )
2653+ }
2654+
25232655fn discover_module_use_path ( module_root : & Path , dep_name : & str ) -> Result < String > {
2656+ let subdir = select_module_subdir ( module_root, dep_name) ?;
2657+ Ok ( module_use_path ( dep_name, & subdir) )
2658+ }
2659+
2660+ fn select_module_subdir ( module_root : & Path , dep_name : & str ) -> Result < PathBuf > {
25242661 let metadata = read_nupm_metadata_hints ( module_root) ?;
25252662 let mut candidates = Vec :: new ( ) ;
25262663 let mut seen = HashSet :: new ( ) ;
@@ -2564,13 +2701,13 @@ fn discover_module_use_path(module_root: &Path, dep_name: &str) -> Result<String
25642701 }
25652702
25662703 if let Some ( best) = candidates. first ( ) {
2567- return Ok ( module_use_path ( dep_name , best) ) ;
2704+ return Ok ( best. clone ( ) ) ;
25682705 }
25692706
25702707 ui:: warn ( format ! (
25712708 "could not locate mod.nu for module '{dep_name}' after install; defaulting to '{dep_name}'"
25722709 ) ) ;
2573- Ok ( dep_name . to_string ( ) )
2710+ Ok ( PathBuf :: new ( ) )
25742711}
25752712
25762713fn read_nupm_metadata_hints ( module_root : & Path ) -> Result < NupmMetadataHints > {
@@ -3087,6 +3224,76 @@ mod tests {
30873224 let _ = std:: fs:: remove_dir_all ( module_root) ;
30883225 }
30893226
3227+ #[ test]
3228+ fn write_module_dist_info_excludes_nested_module_subdir ( ) {
3229+ let repo_root = make_temp_dir ( "dist_info_nested" ) ;
3230+ let module_dir = repo_root. join ( "nu-salesforce" ) ;
3231+ std:: fs:: create_dir_all ( & module_dir) . unwrap ( ) ;
3232+ std:: fs:: write ( module_dir. join ( "mod.nu" ) , "# module" ) . unwrap ( ) ;
3233+ std:: fs:: write ( repo_root. join ( "README.md" ) , "docs" ) . unwrap ( ) ;
3234+ std:: fs:: write ( repo_root. join ( "LICENSE" ) , "license" ) . unwrap ( ) ;
3235+ std:: fs:: create_dir_all ( repo_root. join ( "docs" ) ) . unwrap ( ) ;
3236+ std:: fs:: write ( repo_root. join ( "docs" ) . join ( "usage.md" ) , "usage" ) . unwrap ( ) ;
3237+
3238+ let dist_info =
3239+ make_temp_dir ( "dist_info_nested_out" ) . join ( "nu-salesforce-v0.1.0.dist-info" ) ;
3240+ write_module_dist_info ( & repo_root, Path :: new ( "nu-salesforce" ) , & dist_info) . unwrap ( ) ;
3241+
3242+ assert ! ( dist_info. join( "README.md" ) . is_file( ) ) ;
3243+ assert ! ( dist_info. join( "LICENSE" ) . is_file( ) ) ;
3244+ assert ! ( dist_info. join( "docs" ) . join( "usage.md" ) . is_file( ) ) ;
3245+ assert ! ( !dist_info. join( "nu-salesforce" ) . exists( ) ) ;
3246+
3247+ let _ = std:: fs:: remove_dir_all ( repo_root) ;
3248+ let _ = std:: fs:: remove_dir_all ( dist_info. parent ( ) . unwrap ( ) ) ;
3249+ }
3250+
3251+ #[ test]
3252+ fn write_module_dist_info_root_module_keeps_peripheral_files_only ( ) {
3253+ let repo_root = make_temp_dir ( "dist_info_root" ) ;
3254+ std:: fs:: write ( repo_root. join ( "mod.nu" ) , "# module" ) . unwrap ( ) ;
3255+ std:: fs:: write ( repo_root. join ( "README.md" ) , "docs" ) . unwrap ( ) ;
3256+ std:: fs:: write ( repo_root. join ( "LICENSE" ) , "license" ) . unwrap ( ) ;
3257+ std:: fs:: create_dir_all ( repo_root. join ( "docs" ) ) . unwrap ( ) ;
3258+ std:: fs:: write ( repo_root. join ( "docs" ) . join ( "guide.md" ) , "guide" ) . unwrap ( ) ;
3259+
3260+ let dist_info = make_temp_dir ( "dist_info_root_out" ) . join ( "nu-foo-v0.1.0.dist-info" ) ;
3261+ write_module_dist_info ( & repo_root, Path :: new ( "" ) , & dist_info) . unwrap ( ) ;
3262+
3263+ assert ! ( dist_info. join( "README.md" ) . is_file( ) ) ;
3264+ assert ! ( dist_info. join( "LICENSE" ) . is_file( ) ) ;
3265+ assert ! ( dist_info. join( "docs" ) . join( "guide.md" ) . is_file( ) ) ;
3266+ assert ! ( !dist_info. join( "mod.nu" ) . exists( ) ) ;
3267+
3268+ let _ = std:: fs:: remove_dir_all ( repo_root) ;
3269+ let _ = std:: fs:: remove_dir_all ( dist_info. parent ( ) . unwrap ( ) ) ;
3270+ }
3271+
3272+ #[ test]
3273+ fn dist_info_dir_name_uses_tag_or_short_rev ( ) {
3274+ let with_tag = ResolvedDep {
3275+ name : "nu-salesforce" . to_string ( ) ,
3276+ git : "https://example.com/nu-salesforce.git" . to_string ( ) ,
3277+ tag : Some ( "v0.3.0" . to_string ( ) ) ,
3278+ rev : "0123456789abcdef" . to_string ( ) ,
3279+ } ;
3280+ let without_tag = ResolvedDep {
3281+ name : "nu-salesforce" . to_string ( ) ,
3282+ git : "https://example.com/nu-salesforce.git" . to_string ( ) ,
3283+ tag : None ,
3284+ rev : "0123456789abcdef" . to_string ( ) ,
3285+ } ;
3286+
3287+ assert_eq ! (
3288+ dist_info_dir_name( & with_tag) ,
3289+ "nu-salesforce-v0.3.0.dist-info"
3290+ ) ;
3291+ assert_eq ! (
3292+ dist_info_dir_name( & without_tag) ,
3293+ "nu-salesforce-0123456789ab.dist-info"
3294+ ) ;
3295+ }
3296+
30903297 #[ test]
30913298 fn local_lockfile_staleness_detects_module_mismatches ( ) {
30923299 let manifest = Manifest :: from_str (
0 commit comments