Skip to content

Commit 370e760

Browse files
committed
feat: extract module info to .dist-info dir
Since the standard module project structure is `<module-name>/<module-name>`, modules are now extracted to `.nu-env/modules/<module-name>` with the package information going to `.nu-env/modules/<module-name>-<version>.dist-info`, like in Python. You now only need to type `use nu-salesforce *` in your script instead of `use nu-salesforce/nu-salesforce *`.
1 parent 1ec689e commit 370e760

3 files changed

Lines changed: 286 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Unreleased
22

3+
# Changed
4+
5+
- Since the standard module project structure is `<module-name>/<module-name>`, modules are now extracted to `.nu-env/modules/<module-name>` with the package information going to `.nu-env/modules/<module-name>-<version>.dist-info`, like in Python. You now only need to type `use nu-salesforce *` in your script instead of `use nu-salesforce/nu-salesforce *`.
6+
37
# Version 0.3.3 (2026-03-06)
48

59
## Added

src/installer.rs

Lines changed: 210 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
25232655
fn 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

25762713
fn 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(

src/main.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,9 @@ fn cmd_remove(dir: &Path, name: String) -> Result<()> {
448448
std::fs::remove_dir_all(&module_dir)?;
449449
eprintln!("Removed .nu-env/modules/{name}/");
450450
}
451+
for removed in remove_module_dist_info_dirs(&dir.join(".nu-env").join("modules"), &name)? {
452+
eprintln!("Removed .nu-env/modules/{removed}/");
453+
}
451454

452455
// Update lockfile: remove the module package entry
453456
let lock_path = dir.join("quiver.lock");
@@ -524,6 +527,9 @@ fn cmd_remove_global(name: String) -> Result<()> {
524527
std::fs::remove_dir_all(&module_dir)?;
525528
eprintln!("Removed {}/", module_dir.display());
526529
}
530+
for removed in remove_module_dist_info_dirs(&modules_dir, &name)? {
531+
eprintln!("Removed {}/", modules_dir.join(&removed).display());
532+
}
527533

528534
// Update global lockfile
529535
let lock_path = config::global_lock_path()?;
@@ -932,6 +938,9 @@ fn list_installed_module_names(modules_dir: &Path) -> Result<Vec<String>> {
932938
}
933939

934940
if let Some(name) = entry.file_name().to_str() {
941+
if name.ends_with(".dist-info") {
942+
continue;
943+
}
935944
modules.push(name.to_string());
936945
}
937946
}
@@ -940,6 +949,37 @@ fn list_installed_module_names(modules_dir: &Path) -> Result<Vec<String>> {
940949
Ok(modules)
941950
}
942951

952+
fn remove_module_dist_info_dirs(modules_dir: &Path, module_name: &str) -> Result<Vec<String>> {
953+
if !modules_dir.exists() {
954+
return Ok(Vec::new());
955+
}
956+
957+
let mut removed = Vec::new();
958+
let prefix = format!("{module_name}-");
959+
960+
for entry in std::fs::read_dir(modules_dir)? {
961+
let entry = entry?;
962+
let path = entry.path();
963+
if !path.is_dir() {
964+
continue;
965+
}
966+
967+
let file_name = entry.file_name();
968+
let Some(name) = file_name.to_str() else {
969+
continue;
970+
};
971+
if !name.starts_with(&prefix) || !name.ends_with(".dist-info") {
972+
continue;
973+
}
974+
975+
std::fs::remove_dir_all(&path)?;
976+
removed.push(name.to_string());
977+
}
978+
979+
removed.sort();
980+
Ok(removed)
981+
}
982+
943983
fn list_installed_plugin_names(bin_dir: &Path) -> Result<Vec<String>> {
944984
if !bin_dir.exists() {
945985
return Ok(Vec::new());
@@ -1325,6 +1365,7 @@ mod tests {
13251365
let modules_dir = make_temp_dir("list_modules");
13261366
std::fs::create_dir_all(modules_dir.join("nu-zeta")).unwrap();
13271367
std::fs::create_dir_all(modules_dir.join("nu-alpha")).unwrap();
1368+
std::fs::create_dir_all(modules_dir.join("nu-alpha-v0.1.0.dist-info")).unwrap();
13281369
std::fs::write(modules_dir.join("activate.nu"), "# generated").unwrap();
13291370

13301371
let modules = list_installed_module_names(&modules_dir).unwrap();
@@ -1342,6 +1383,37 @@ mod tests {
13421383
let _ = std::fs::remove_dir_all(root_dir);
13431384
}
13441385

1386+
#[test]
1387+
fn remove_module_dist_info_dirs_removes_only_matching_module_dist_info() {
1388+
let modules_dir = make_temp_dir("remove_dist_info");
1389+
std::fs::create_dir_all(modules_dir.join("nu-salesforce")).unwrap();
1390+
std::fs::create_dir_all(modules_dir.join("nu-salesforce-v0.3.0.dist-info")).unwrap();
1391+
std::fs::create_dir_all(modules_dir.join("nu-salesforce-abcdef123456.dist-info")).unwrap();
1392+
std::fs::create_dir_all(modules_dir.join("nu-other-v1.0.0.dist-info")).unwrap();
1393+
std::fs::create_dir_all(modules_dir.join("nu-salesforce.dist-info")).unwrap();
1394+
1395+
let removed = remove_module_dist_info_dirs(&modules_dir, "nu-salesforce").unwrap();
1396+
1397+
assert_eq!(
1398+
removed,
1399+
vec![
1400+
"nu-salesforce-abcdef123456.dist-info".to_string(),
1401+
"nu-salesforce-v0.3.0.dist-info".to_string()
1402+
]
1403+
);
1404+
assert!(modules_dir.join("nu-salesforce").is_dir());
1405+
assert!(modules_dir.join("nu-other-v1.0.0.dist-info").is_dir());
1406+
assert!(modules_dir.join("nu-salesforce.dist-info").is_dir());
1407+
assert!(!modules_dir.join("nu-salesforce-v0.3.0.dist-info").exists());
1408+
assert!(
1409+
!modules_dir
1410+
.join("nu-salesforce-abcdef123456.dist-info")
1411+
.exists()
1412+
);
1413+
1414+
let _ = std::fs::remove_dir_all(modules_dir);
1415+
}
1416+
13451417
#[test]
13461418
fn list_installed_plugin_names_returns_sorted_plugin_binaries() {
13471419
let bin_dir = make_temp_dir("list_plugins");

0 commit comments

Comments
 (0)