Release v0.2.2: merge dev into main
This commit is contained in:
commit
8b63f97cd6
11 files changed
+519
-18
No files matched your search
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.2
|
||||
|
||||
- Use `UniLoader_Global` and `UniLoader_Local` nicknames for global and project libraries, preventing project entries from shadowing global managed libraries.
|
||||
- Keep existing asset filenames and portable paths unchanged.
|
||||
- Add `doctor --global` and `doctor --project FILE` to preview nickname repairs; `--fix` backs up and migrates registrations, managed symbol bindings, and project schematic/PCB references.
|
||||
- Preserve unrelated registrations, reject occupied target nicknames before writes, and retain exact legacy nickname mappings for later project migrations.
|
||||
- Stop imports using legacy managed nicknames with instructions to run doctor.
|
||||
|
||||
Validation: 62 automated tests, Clippy, formatting, and release build passed. Live KiCad integration requires manual verification.
|
||||
|
||||
## 0.2.1
|
||||
|
||||
- Fix high idle watcher CPU usage caused by directory scans generating filesystem open events and triggering further scans.
|
||||
|
||||
Generated
+1
-1
@@ -2869,7 +2869,7 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "uniloader"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "uniloader"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ folder throughout your graphical login session.
|
||||
|
||||
It supports symbols, footprints and referenced STEP/VRML models, with:
|
||||
|
||||
- Global libraries or portable project-local libraries, created and registered automatically.
|
||||
- Global (`UniLoader_Global`) and project-local (`UniLoader_Local`) library nicknames, created and registered automatically.
|
||||
- A destination dialog offering detected KiCad project suggestions, Global and Browse.
|
||||
- Conservative exact footprint reuse and comparison dialogs for uncertain matches.
|
||||
- Broken model-path repair, explicit ambiguous model choices and missing-model warnings.
|
||||
@@ -59,6 +59,22 @@ No-argument startup also runs the watcher. `--noninteractive` leaves unresolved
|
||||
choices pending; `--force` on manual imports deliberately revisits a completed
|
||||
source. Check an imported component in KiCad before using it in a board.
|
||||
|
||||
## Upgrade existing libraries to v0.2.2
|
||||
|
||||
Close KiCad and stop the watcher, then preview and apply the nickname migration:
|
||||
|
||||
```sh
|
||||
uniloader doctor --global
|
||||
uniloader doctor --global --fix
|
||||
uniloader doctor --project /path/to/Board/Board.kicad_pro
|
||||
uniloader doctor --project /path/to/Board/Board.kicad_pro --fix
|
||||
```
|
||||
|
||||
Repeat the project commands for each existing project. Project repair also checks
|
||||
and repairs the configured global libraries. Existing library filenames stay the
|
||||
same. Changed files are backed up; see the [setup guide](docs/SETUP.md#nickname-migration-v022)
|
||||
for migration scope and recovery. Reopen KiCad afterwards.
|
||||
|
||||
## Scope
|
||||
|
||||
This is practical Linux development tooling. Project discovery currently uses
|
||||
|
||||
@@ -129,7 +129,7 @@ project/
|
||||
models/
|
||||
```
|
||||
|
||||
- Add/merge UniLoader entries into the appropriate symbol and footprint tables.
|
||||
- Add/merge `UniLoader_Global` or `UniLoader_Local` entries into the appropriate symbol and footprint tables.
|
||||
Preserve unrelated entries and never silently repoint an occupied nickname.
|
||||
- Project URIs use `${KIPRJMOD}/libraries/...`; global URIs use stable absolute
|
||||
paths. Let users select the KiCad configuration/version directory if ambiguous.
|
||||
|
||||
+46
-1
@@ -152,7 +152,9 @@ Use `--noninteractive` to avoid opening dialogs. A pending choice is not consent
|
||||
Project imports create `libraries/UniLoader.kicad_sym`,
|
||||
`libraries/UniLoader.pretty/`, and model storage as needed, and merge registration
|
||||
entries into `sym-lib-table` and `fp-lib-table` beside the `.kicad_pro` file.
|
||||
Project paths use `${KIPRJMOD}`. Portable mode copies selected external footprints
|
||||
Project registration uses the nickname `UniLoader_Local`; global registration uses
|
||||
`UniLoader_Global`. Library filenames remain `UniLoader.kicad_sym` and
|
||||
`UniLoader.pretty` in both scopes. Project paths use `${KIPRJMOD}`. Portable mode copies selected external footprints
|
||||
and available referenced models into the project. Global imports use the
|
||||
configured library root and register entries in the selected KiCad configuration.
|
||||
Existing unrelated table entries are preserved. Existing schematic and PCB
|
||||
@@ -170,6 +172,49 @@ assets. Source archives are preserved by default. Incomplete or invalid ZIPs
|
||||
are retried for the configured period, then remain recoverable rather than
|
||||
stopping subsequent downloads.
|
||||
|
||||
## Nickname migration (v0.2.2)
|
||||
|
||||
Before upgrading existing libraries, close KiCad and stop the watcher:
|
||||
|
||||
```sh
|
||||
systemctl --user stop uniloader.service
|
||||
uniloader doctor --global
|
||||
uniloader doctor --global --fix
|
||||
uniloader doctor --project /path/to/Board/Board.kicad_pro
|
||||
uniloader doctor --project /path/to/Board/Board.kicad_pro --fix
|
||||
systemctl --user start uniloader.service
|
||||
```
|
||||
|
||||
Without `--fix`, doctor previews the files it would change. `--global` checks the
|
||||
configured global tables and managed symbol library. `--project` checks those
|
||||
plus the selected project's tables, managed symbol library, and `.kicad_sch` and
|
||||
`.kicad_pcb` files recursively inside its directory (including subsheets). Run it
|
||||
for every existing project; projects elsewhere are not automatically discovered
|
||||
or modified. External subsheets must be migrated separately. Set
|
||||
`libraries.kicad_config` to the correct version directory when needed.
|
||||
|
||||
Doctor identifies managed registrations by their resolved library paths, preserves
|
||||
unrelated registrations and metadata, and refuses an occupied target nickname.
|
||||
It updates symbol footprint properties, schematic library IDs and cached symbol
|
||||
IDs, and placed PCB footprint IDs according to the original project-over-global
|
||||
resolution. An old ambiguous ID keeps that original resolution; doctor cannot
|
||||
recover a different intended library. Asset filenames and model paths stay the
|
||||
same. Changed S-expression files are reserialized, so whitespace may change.
|
||||
|
||||
Repairs use the import writer lock, atomic file replacement, and the normal
|
||||
backup/history mechanism. The command prints backup paths under
|
||||
`${XDG_DATA_HOME:-~/.local/share}/uniloader/backups/`; `uniloader pending --all`
|
||||
also lists changed files and backups. Exact old nicknames (including collision
|
||||
suffixes) are retained under `uniloader/nickname-migrations/` in the same data
|
||||
root so later project repairs can update global references. Keep this migration
|
||||
history until all projects are repaired. Repeated repairs leave unchanged files
|
||||
alone. All files are parsed and collisions checked before writing, but a disk
|
||||
failure can still interrupt a multi-file repair; restore the listed backups if
|
||||
needed. Reopen KiCad after repair to reload its cached tables.
|
||||
|
||||
Imports into a destination still using an old managed nickname stop with a doctor
|
||||
instruction. Repair that destination before resuming imports.
|
||||
|
||||
## 3D models and broken paths
|
||||
|
||||
STEP (`.step`/`.stp`) and VRML (`.wrl`) models are copied when referenced by an
|
||||
|
||||
@@ -66,6 +66,13 @@ impl Destination {
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
pub fn nickname(&self) -> &'static str {
|
||||
if self.project.is_some() {
|
||||
"UniLoader_Local"
|
||||
} else {
|
||||
"UniLoader_Global"
|
||||
}
|
||||
}
|
||||
fn uri(&self, path: &Path) -> String {
|
||||
if let Some(project) = &self.project
|
||||
&& let Ok(relative) = path.strip_prefix(project)
|
||||
@@ -144,6 +151,9 @@ pub fn register_table(
|
||||
.then(|| field_value(entry, "name").map(str::to_owned))
|
||||
.flatten()
|
||||
}) {
|
||||
if requested_nickname.starts_with("UniLoader_") && !name.starts_with(requested_nickname) {
|
||||
bail!("Legacy managed library nickname {name}; run `uniloader doctor --global --fix` or `uniloader doctor --project /path/to/Board.kicad_pro --fix` before importing");
|
||||
}
|
||||
return Ok((format!("{table}\n"), name));
|
||||
}
|
||||
let mut nickname = requested_nickname.to_owned();
|
||||
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
//! Explicit, backed-up migration of managed library registrations and references.
|
||||
use crate::{
|
||||
config::Config,
|
||||
destination::{self, Destination},
|
||||
importer,
|
||||
sexpr::{Node, parse},
|
||||
storage::{Lock, Plan, Record},
|
||||
};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::{collections::BTreeMap, fs, path::Path};
|
||||
|
||||
type Renames = BTreeMap<String, String>;
|
||||
|
||||
fn table(dest: &Destination, kind: &str, vars: &Renames, plan: &mut Plan) -> Result<Renames> {
|
||||
let (file, target) = if kind == "sym_lib_table" {
|
||||
("sym-lib-table", &dest.symbol_path)
|
||||
} else {
|
||||
("fp-lib-table", &dest.pretty_dir)
|
||||
};
|
||||
let path = dest.table_dir.join(file);
|
||||
if !path.exists() {
|
||||
return Ok(Renames::new());
|
||||
}
|
||||
let original = fs::read_to_string(&path)?;
|
||||
let mut node = parse(&original)?;
|
||||
if node.head() != Some(kind) {
|
||||
bail!("Invalid table {}", path.display());
|
||||
}
|
||||
// Keep exact old names so a later project migration also handles legacy
|
||||
// collision suffixes without guessing which nickname used to be ours.
|
||||
let history_path = crate::config::data_dir()
|
||||
.join("nickname-migrations")
|
||||
.join(format!(
|
||||
"{}.json",
|
||||
crate::storage::hash(path.to_string_lossy().as_bytes())
|
||||
));
|
||||
let mut history: Renames = if history_path.exists() {
|
||||
serde_json::from_slice(&fs::read(&history_path)?)?
|
||||
} else {
|
||||
Renames::new()
|
||||
};
|
||||
let original_history = history.clone();
|
||||
let mut changes = Renames::new();
|
||||
let mut occupied = BTreeMap::new();
|
||||
for entry in node.children().iter().filter(|n| n.head() == Some("lib")) {
|
||||
let name = entry
|
||||
.field("name")
|
||||
.and_then(Node::value)
|
||||
.context("Library entry missing name")?;
|
||||
if occupied.insert(name.to_owned(), ()).is_some() {
|
||||
bail!("Duplicate nickname {name} in {}", path.display());
|
||||
}
|
||||
}
|
||||
for entry in node
|
||||
.children_mut()
|
||||
.iter_mut()
|
||||
.filter(|n| n.head() == Some("lib"))
|
||||
{
|
||||
let name = entry
|
||||
.field("name")
|
||||
.and_then(Node::value)
|
||||
.context("Library entry missing name")?
|
||||
.to_owned();
|
||||
// Only change entries that resolve to our managed file/directory.
|
||||
let uri = entry
|
||||
.field("uri")
|
||||
.and_then(Node::value)
|
||||
.context("Library entry missing URI")?;
|
||||
let Ok(resolved) = destination::resolve_variables(uri, dest.project.as_deref(), vars)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let resolved = std::path::PathBuf::from(resolved);
|
||||
let resolved = if resolved.is_absolute() {
|
||||
resolved
|
||||
} else {
|
||||
dest.table_dir.join(resolved)
|
||||
};
|
||||
let matches = resolved == *target
|
||||
|| (target.exists() && resolved.canonicalize().ok() == target.canonicalize().ok());
|
||||
if !matches || entry.field("type").and_then(Node::value) != Some("KiCad") {
|
||||
continue;
|
||||
}
|
||||
let wanted = dest.nickname();
|
||||
if name == wanted
|
||||
|| name
|
||||
.strip_prefix(wanted)
|
||||
.is_some_and(|s| s.starts_with('_'))
|
||||
{
|
||||
changes.extend(
|
||||
history
|
||||
.iter()
|
||||
.filter(|(old, new)| *new == &name && !occupied.contains_key(*old))
|
||||
.map(|(old, new)| (old.clone(), new.clone())),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if occupied.contains_key(wanted) {
|
||||
bail!(
|
||||
"Nickname {wanted} is occupied in {}; resolve it before migration",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
occupied.insert(wanted.into(), ());
|
||||
entry.field_mut("name").unwrap().children_mut()[1] = Node::string(wanted);
|
||||
history.insert(name.clone(), wanted.into());
|
||||
changes.insert(name, wanted.into());
|
||||
}
|
||||
if history != original_history {
|
||||
plan.put(history_path, serde_json::to_vec_pretty(&history)?)?;
|
||||
}
|
||||
if node != parse(&original)? {
|
||||
plan.put(path, format!("{node}\n").into_bytes())?;
|
||||
}
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
fn rename(value: &mut Node, mapping: &Renames) {
|
||||
if let Node::Atom(s) | Node::Str(s) = value
|
||||
&& let Some((nick, item)) = s.split_once(':')
|
||||
&& let Some(new) = mapping.get(nick)
|
||||
{
|
||||
*s = format!("{new}:{item}");
|
||||
}
|
||||
}
|
||||
fn references(node: &mut Node, symbols: &Renames, footprints: &Renames, parent: Option<&str>) {
|
||||
let head = node.head().map(str::to_owned);
|
||||
if let Node::List(items) = node {
|
||||
match head.as_deref() {
|
||||
Some("lib_id") | Some("extends") => {
|
||||
if let Some(v) = items.get_mut(1) {
|
||||
rename(v, symbols);
|
||||
}
|
||||
}
|
||||
Some("symbol") if parent == Some("lib_symbols") => {
|
||||
if let Some(v) = items.get_mut(1) {
|
||||
rename(v, symbols);
|
||||
}
|
||||
}
|
||||
Some("footprint") => {
|
||||
if let Some(v) = items.get_mut(1) {
|
||||
rename(v, footprints);
|
||||
}
|
||||
}
|
||||
Some("property") if items.get(1).and_then(Node::atom) == Some("Footprint") => {
|
||||
if let Some(v) = items.get_mut(2) {
|
||||
rename(v, footprints);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for child in items {
|
||||
references(child, symbols, footprints, head.as_deref());
|
||||
}
|
||||
}
|
||||
}
|
||||
fn patch(path: &Path, symbols: &Renames, footprints: &Renames, plan: &mut Plan) -> Result<()> {
|
||||
if !path.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
let original = fs::read_to_string(path).with_context(|| format!("Read {}", path.display()))?;
|
||||
let mut node = parse(&original).with_context(|| format!("Parse {}", path.display()))?;
|
||||
let before = node.clone();
|
||||
references(&mut node, symbols, footprints, None);
|
||||
if node != before {
|
||||
plan.put(path.to_owned(), format!("{node}\n").into_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
// A local entry shadows the global mapping even if it belongs to another tool.
|
||||
fn overlay(dest: &Destination, file: &str, global: &Renames, local: Renames) -> Result<Renames> {
|
||||
let mut result = global.clone();
|
||||
let path = dest.table_dir.join(file);
|
||||
if path.exists() {
|
||||
for entry in parse(&fs::read_to_string(path)?)?.children() {
|
||||
if entry.head() == Some("lib")
|
||||
&& let Some(name) = entry.field("name").and_then(Node::value)
|
||||
{
|
||||
result.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.extend(local);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn run(cfg: &Config, project: Option<&Path>, fix: bool) -> Result<()> {
|
||||
let _lock = Lock::acquire()?;
|
||||
let kicad = importer::kicad_config(cfg, project.is_none())?;
|
||||
let global = Destination::new(&cfg.libraries.path, None, &kicad)?;
|
||||
let mut plan = Plan::default();
|
||||
let sym = table(
|
||||
&global,
|
||||
"sym_lib_table",
|
||||
&cfg.libraries.variables,
|
||||
&mut plan,
|
||||
)?;
|
||||
let fp = table(&global, "fp_lib_table", &cfg.libraries.variables, &mut plan)?;
|
||||
patch(&global.symbol_path, &sym, &fp, &mut plan)?;
|
||||
if let Some(project) = project {
|
||||
let local = Destination::new(&cfg.libraries.path, Some(project), &kicad)?;
|
||||
let local_sym = table(&local, "sym_lib_table", &cfg.libraries.variables, &mut plan)?;
|
||||
let local_fp = table(&local, "fp_lib_table", &cfg.libraries.variables, &mut plan)?;
|
||||
let sym = overlay(&local, "sym-lib-table", &sym, local_sym)?;
|
||||
let fp = overlay(&local, "fp-lib-table", &fp, local_fp)?;
|
||||
patch(&local.symbol_path, &sym, &fp, &mut plan)?;
|
||||
for entry in walkdir::WalkDir::new(local.project.as_ref().unwrap()).follow_links(false) {
|
||||
let entry = entry?;
|
||||
if entry.file_type().is_file()
|
||||
&& entry
|
||||
.path()
|
||||
.extension()
|
||||
.is_some_and(|e| e == "kicad_sch" || e == "kicad_pcb")
|
||||
{
|
||||
patch(entry.path(), &sym, &fp, &mut plan)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for path in plan.writes.keys() {
|
||||
println!(
|
||||
"{}: {}",
|
||||
if fix { "Repair" } else { "Would repair" },
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if plan.writes.is_empty() {
|
||||
println!("No nickname repairs needed.");
|
||||
} else if fix {
|
||||
let mut record = Record::new(
|
||||
global.root.clone(),
|
||||
crate::storage::hash(b"doctor-nicknames"),
|
||||
);
|
||||
record.id = format!(
|
||||
"doctor-{}",
|
||||
chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
|
||||
);
|
||||
record.status = "importing".into();
|
||||
plan.commit(&mut record)?;
|
||||
record.status = "completed".into();
|
||||
record.message = "Library nickname migration completed".into();
|
||||
record.save()?;
|
||||
for backup in record.backups {
|
||||
println!("Backup: {}", backup.display());
|
||||
}
|
||||
} else {
|
||||
println!("Preview only; rerun with --fix to apply repairs.");
|
||||
}
|
||||
println!(
|
||||
"Run doctor --project /path/to/Board.kicad_pro for each existing project. Close KiCad and stop the watcher before --fix; reopen KiCad afterwards."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
+2
-2
@@ -431,13 +431,13 @@ pub fn run(cfg: &Config, req: &Request, record: &mut Record, session: &mut Sessi
|
||||
let (fp_table, fp_nickname) = destination::register_table(
|
||||
text(&dest.table_dir.join("fp-lib-table"))?.as_deref(),
|
||||
"fp_lib_table",
|
||||
"UniLoader",
|
||||
dest.nickname(),
|
||||
&dest.footprint_uri(),
|
||||
)?;
|
||||
let (sym_table, sym_nickname) = destination::register_table(
|
||||
text(&dest.table_dir.join("sym-lib-table"))?.as_deref(),
|
||||
"sym_lib_table",
|
||||
"UniLoader",
|
||||
dest.nickname(),
|
||||
&dest.symbol_uri(),
|
||||
)?;
|
||||
let mut library_index = index(cfg, &dest, &kicad, &vars, &fp_nickname)
|
||||
|
||||
+15
@@ -3,6 +3,7 @@
|
||||
mod config;
|
||||
mod desktop;
|
||||
mod destination;
|
||||
mod doctor;
|
||||
mod gui;
|
||||
mod importer;
|
||||
mod matching;
|
||||
@@ -74,6 +75,15 @@ enum Commands {
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
/// Check managed nicknames; --fix backs up and migrates libraries/references.
|
||||
Doctor {
|
||||
#[arg(long, required_unless_present = "project", conflicts_with = "project")]
|
||||
global: bool,
|
||||
#[arg(long)]
|
||||
project: Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
fix: bool,
|
||||
},
|
||||
/// Show project suggestions from running KiCad processes.
|
||||
Discover,
|
||||
}
|
||||
@@ -392,6 +402,11 @@ fn main() -> Result<()> {
|
||||
&mut Session::default(),
|
||||
)
|
||||
}
|
||||
Commands::Doctor {
|
||||
global: _,
|
||||
project,
|
||||
fix,
|
||||
} => doctor::run(&cfg, project.as_deref(), fix),
|
||||
Commands::Discover => {
|
||||
let p = desktop::projects();
|
||||
if p.is_empty() {
|
||||
|
||||
+164
-11
@@ -180,7 +180,7 @@ fn imports_three_provider_layouts_and_preserves_original_archives() {
|
||||
assert!(source.is_file());
|
||||
assert_eq!(h.footprints().len(), 1);
|
||||
let lib = h.library();
|
||||
assert_eq!(link(symbols(&lib)[0]), "UniLoader:Package");
|
||||
assert_eq!(link(symbols(&lib)[0]), "UniLoader_Local:Package");
|
||||
assert!(lib.to_string().contains("µ Ω (quoted)"));
|
||||
assert_eq!(lib.field("version").unwrap().value(), Some("20231120"));
|
||||
}
|
||||
@@ -208,7 +208,11 @@ fn exact_cross_provider_reuse_and_repeat_import_are_idempotent() {
|
||||
assert_eq!(h.footprints().len(), 1);
|
||||
let lib = h.library();
|
||||
assert_eq!(symbols(&lib).len(), 2);
|
||||
assert!(symbols(&lib).iter().all(|s| link(s) == "UniLoader:PkgA"));
|
||||
assert!(
|
||||
symbols(&lib)
|
||||
.iter()
|
||||
.all(|s| link(s) == "UniLoader_Local:PkgA")
|
||||
);
|
||||
ok(&h.import(&a, &[]));
|
||||
ok(&h.import(&b, &["--force"]));
|
||||
assert_eq!(symbols(&h.library()).len(), 2);
|
||||
@@ -257,7 +261,7 @@ fn uncertain_footprints_stay_pending_then_retry_with_separate_policy() {
|
||||
.into_iter()
|
||||
.find(|s| s.value() == Some("Second"))
|
||||
.unwrap();
|
||||
assert!(link(second).starts_with("UniLoader:Package_"));
|
||||
assert!(link(second).starts_with("UniLoader_Local:Package_"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -361,8 +365,8 @@ fn multi_symbol_footprint_bindings_do_not_use_first_footprint_for_all() {
|
||||
ok(&h.import(&source, &[]));
|
||||
let lib = h.library();
|
||||
for (name, expected) in [
|
||||
("First", "UniLoader:PackageA"),
|
||||
("Second", "UniLoader:PackageB"),
|
||||
("First", "UniLoader_Local:PackageA"),
|
||||
("Second", "UniLoader_Local:PackageB"),
|
||||
] {
|
||||
let s = symbols(&lib)
|
||||
.into_iter()
|
||||
@@ -485,7 +489,10 @@ fn portable_project_copies_exact_global_footprint_and_model() {
|
||||
.unwrap()
|
||||
.collect();
|
||||
assert_eq!(copied_models.len(), 1);
|
||||
assert_eq!(link(symbols(&h.library())[0]), "UniLoader:GlobalPackage");
|
||||
assert_eq!(
|
||||
link(symbols(&h.library())[0]),
|
||||
"UniLoader_Local:GlobalPackage"
|
||||
);
|
||||
}
|
||||
|
||||
fn records(h: &Harness) -> Vec<serde_json::Value> {
|
||||
@@ -516,11 +523,14 @@ fn symbol_only_global_dependency_is_copied_into_portable_project() {
|
||||
ok(&h.cmd(&["import", global.to_str().unwrap(), "--global"]));
|
||||
let source = h.bundle(
|
||||
"symbol-only",
|
||||
&[("b.kicad_sym", symbol("LocalPart", "UniLoader:Package"))],
|
||||
&[(
|
||||
"b.kicad_sym",
|
||||
symbol("LocalPart", "UniLoader_Global:Package"),
|
||||
)],
|
||||
);
|
||||
ok(&h.import(&source, &[]));
|
||||
assert_eq!(h.footprints().len(), 1);
|
||||
assert_eq!(link(symbols(&h.library())[0]), "UniLoader:Package");
|
||||
assert_eq!(link(symbols(&h.library())[0]), "UniLoader_Local:Package");
|
||||
assert!(
|
||||
fs::read_to_string(&h.footprints()[0])
|
||||
.unwrap()
|
||||
@@ -603,7 +613,7 @@ fn project_library_and_model_paths_resolve_after_directory_relocation() {
|
||||
assert_eq!(fs::read(path).unwrap(), b"RELOCATED MODEL");
|
||||
let lib = parse(&fs::read_to_string(relocated.join("libraries/UniLoader.kicad_sym")).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(link(symbols(&lib)[0]), "UniLoader:Package");
|
||||
assert_eq!(link(symbols(&lib)[0]), "UniLoader_Local:Package");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -620,7 +630,7 @@ fn disabled_footprint_import_reuses_registered_assets_and_rejects_dangling_links
|
||||
append_config(&h, "[options]\nfootprints = false");
|
||||
let valid = h.bundle(
|
||||
"valid",
|
||||
&[("b.kicad_sym", symbol("Second", "UniLoader:Package"))],
|
||||
&[("b.kicad_sym", symbol("Second", "UniLoader_Local:Package"))],
|
||||
);
|
||||
ok(&h.import(&valid, &[]));
|
||||
let invalid = h.bundle(
|
||||
@@ -774,7 +784,7 @@ fn disabled_footprints_cannot_reference_an_unregistered_managed_directory() {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(table.children().iter().any(|n| n.head() == Some("lib")
|
||||
&& n.field("name").and_then(Node::value) == Some("UniLoader")));
|
||||
&& n.field("name").and_then(Node::value) == Some("UniLoader_Local")));
|
||||
} else {
|
||||
assert!(output(&result).contains("pending"));
|
||||
assert!(
|
||||
@@ -816,3 +826,146 @@ fn qualified_existing_reference_is_not_silently_retargeted_by_bundle_basename()
|
||||
assert!(output(&result).contains("pending"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctor_migrates_both_scopes_and_project_references_with_backups() {
|
||||
let h = Harness::new("separate");
|
||||
let source = h.bundle(
|
||||
"source",
|
||||
&[
|
||||
("a.kicad_sym", symbol("Part", "Package")),
|
||||
("Package.kicad_mod", footprint("Package", 1.0, "")),
|
||||
],
|
||||
);
|
||||
ok(&h.cmd(&["import", source.to_str().unwrap(), "--global"]));
|
||||
ok(&h.import(&source, &[]));
|
||||
// Recreate v0.2.1 registrations and stored footprint bindings.
|
||||
let paths = [
|
||||
"kicad-config/sym-lib-table",
|
||||
"kicad-config/fp-lib-table",
|
||||
"global/UniLoader.kicad_sym",
|
||||
"project/sym-lib-table",
|
||||
"project/fp-lib-table",
|
||||
"project/libraries/UniLoader.kicad_sym",
|
||||
];
|
||||
for path in paths {
|
||||
let path = h.root.join(path);
|
||||
let text = fs::read_to_string(&path)
|
||||
.unwrap()
|
||||
.replace("UniLoader_Global", "UniLoader")
|
||||
.replace("UniLoader_Local", "UniLoader");
|
||||
fs::write(path, text).unwrap();
|
||||
}
|
||||
let sch = h.root.join("project/board.kicad_sch");
|
||||
fs::write(&sch, r#"(kicad_sch (lib_symbols (symbol "UniLoader:Part" (property "Footprint" "UniLoader:Package"))) (symbol (lib_id "UniLoader:Part") (property "Footprint" "UniLoader:Package") (property "Description" "UniLoader:keep")))"#).unwrap();
|
||||
let pcb = h.root.join("project/board.kicad_pcb");
|
||||
fs::write(
|
||||
&pcb,
|
||||
r#"(kicad_pcb (footprint "UniLoader:Package" (property "Value" "keep")))"#,
|
||||
)
|
||||
.unwrap();
|
||||
let before = fs::read(&sch).unwrap();
|
||||
let args = ["doctor", "--project", h.project.to_str().unwrap()];
|
||||
let preview = h.cmd(&args);
|
||||
ok(&preview);
|
||||
assert!(output(&preview).contains("Would repair"));
|
||||
assert_eq!(fs::read(&sch).unwrap(), before);
|
||||
// Imports must not silently retain a legacy registration.
|
||||
let blocked = h.import(&source, &["--force"]);
|
||||
assert!(!blocked.status.success());
|
||||
assert!(output(&blocked).contains("doctor"));
|
||||
let fixed = h.cmd(&["doctor", "--project", h.project.to_str().unwrap(), "--fix"]);
|
||||
ok(&fixed);
|
||||
assert!(output(&fixed).contains("Backup:"));
|
||||
assert!(
|
||||
walkdir::WalkDir::new(h.root.join("xdg-data/uniloader/backups"))
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.any(|entry| entry.file_type().is_file() && fs::read(entry.path()).unwrap() == before)
|
||||
);
|
||||
assert!(
|
||||
fs::read_to_string(&sch)
|
||||
.unwrap()
|
||||
.contains("UniLoader_Local:Part")
|
||||
);
|
||||
assert!(fs::read_to_string(&sch).unwrap().contains("UniLoader:keep"));
|
||||
assert!(
|
||||
fs::read_to_string(&pcb)
|
||||
.unwrap()
|
||||
.contains("UniLoader_Local:Package")
|
||||
);
|
||||
assert_eq!(link(symbols(&h.library())[0]), "UniLoader_Local:Package");
|
||||
let global =
|
||||
parse(&fs::read_to_string(h.root.join("global/UniLoader.kicad_sym")).unwrap()).unwrap();
|
||||
assert_eq!(link(symbols(&global)[0]), "UniLoader_Global:Package");
|
||||
let again = h.cmd(&["doctor", "--project", h.project.to_str().unwrap(), "--fix"]);
|
||||
ok(&again);
|
||||
assert!(output(&again).contains("No nickname repairs needed"));
|
||||
ok(&h.import(&source, &["--force"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctor_refuses_occupied_nickname_before_writing_anything() {
|
||||
let h = Harness::new("separate");
|
||||
fs::create_dir_all(h.root.join("kicad-config")).unwrap();
|
||||
let table = h.root.join("kicad-config/fp-lib-table");
|
||||
let text = format!(
|
||||
r#"(fp_lib_table (lib (name "UniLoader") (type "KiCad") (uri "{}")) (lib (name "UniLoader_Global") (type "KiCad") (uri "/unrelated.pretty") (options "keep")))"#,
|
||||
h.root.join("global/UniLoader.pretty").display()
|
||||
);
|
||||
fs::write(&table, &text).unwrap();
|
||||
let result = h.cmd(&["doctor", "--global", "--fix"]);
|
||||
assert!(!result.status.success());
|
||||
assert!(output(&result).contains("occupied"));
|
||||
assert_eq!(fs::read_to_string(table).unwrap(), text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctor_updates_global_references_in_projects_after_global_migration() {
|
||||
let h = Harness::new("separate");
|
||||
let source = h.bundle(
|
||||
"source",
|
||||
&[
|
||||
("a.kicad_sym", symbol("Part", "Package")),
|
||||
("Package.kicad_mod", footprint("Package", 1.0, "")),
|
||||
],
|
||||
);
|
||||
ok(&h.cmd(&["import", source.to_str().unwrap(), "--global"]));
|
||||
for path in [
|
||||
"kicad-config/fp-lib-table",
|
||||
"kicad-config/sym-lib-table",
|
||||
"global/UniLoader.kicad_sym",
|
||||
] {
|
||||
let path = h.root.join(path);
|
||||
fs::write(
|
||||
&path,
|
||||
fs::read_to_string(&path)
|
||||
.unwrap()
|
||||
.replace("UniLoader_Global", "UniLoader_2"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
ok(&h.cmd(&["doctor", "--global", "--fix"]));
|
||||
let sch = h.root.join("project/board.kicad_sch");
|
||||
let text = r#"(kicad_sch (symbol (lib_id "UniLoader_2:Part") (property "Footprint" "UniLoader_2:Package")))"#;
|
||||
fs::write(&sch, text).unwrap();
|
||||
ok(&h.cmd(&["doctor", "--project", h.project.to_str().unwrap(), "--fix"]));
|
||||
assert!(
|
||||
fs::read_to_string(&sch)
|
||||
.unwrap()
|
||||
.contains("UniLoader_Global:Part")
|
||||
);
|
||||
// An unrelated local registration must retain its existing resolution.
|
||||
fs::write(&sch, text).unwrap();
|
||||
fs::write(
|
||||
h.root.join("project/sym-lib-table"),
|
||||
r#"(sym_lib_table (lib (name "UniLoader_2") (type "KiCad") (uri "/unrelated.kicad_sym")))"#,
|
||||
)
|
||||
.unwrap();
|
||||
ok(&h.cmd(&["doctor", "--project", h.project.to_str().unwrap(), "--fix"]));
|
||||
assert!(
|
||||
fs::read_to_string(&sch)
|
||||
.unwrap()
|
||||
.contains("\"UniLoader_2:Part\"")
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user