KiCad PCB EDA Suite
Loading...
Searching...
No Matches
footprint_import_reconciler.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21
22#include <map>
23#include <set>
24#include <utility>
25
26#include <wx/dir.h>
27#include <wx/filename.h>
28
29#include <board.h>
30#include <footprint.h>
32#include <lib_id.h>
33#include <pad.h>
34#include <project.h>
35#include <project_pcb.h>
36#include <reporter.h>
37#include <string_utils.h>
39#include <io/io_mgr.h>
40#include <pcb_io/pcb_io.h>
41#include <pcb_io/pcb_io_mgr.h>
44
45
47 const wxString& aProjectPath,
48 REPORTER& aReporter ) :
49 m_adapter( aAdapter ),
50 m_projectPath( aProjectPath ),
51 m_reporter( aReporter )
52{
53}
54
55
56namespace
57{
58// structural signature, flags same-name placed instances that differ
59wxString placedSignature( const FOOTPRINT* aFp )
60{
61 BOX2I bbox = aFp->GetBoundingBox( false );
62
63 return wxString::Format( wxS( "%zu:%zu:%lld:%lld" ), aFp->Pads().size(), aFp->GraphicalItems().size(),
64 (long long) bbox.GetWidth(), (long long) bbox.GetHeight() );
65}
66
67
68std::multiset<wxString> padNumbers( const FOOTPRINT& aFp )
69{
70 std::multiset<wxString> numbers;
71
72 for( const PAD* pad : aFp.Pads() )
73 numbers.insert( pad->GetNumber() );
74
75 return numbers;
76}
77
78
79// two tools never draw a footprint identically, so equivalence is the pad set
80bool sameInterface( const FOOTPRINT& aLhs, const FOOTPRINT& aRhs )
81{
82 return padNumbers( aLhs ) == padNumbers( aRhs );
83}
84
85
86// reuse existing row/dir only if prior import-managed cache
87bool isManagedCache( const LIBRARY_TABLE_ROW* aRow )
88{
89 return aRow && aRow->GetOptionsMap().count( IMPORT_PROJ_PROPS::MANAGED_CACHE_KEY ) > 0;
90}
91}
92
93
96 std::vector<std::unique_ptr<FOOTPRINT>> aDefinitions,
97 const wxString& aCacheNickname,
98 const std::vector<wxString>& aSourceLibNicknames )
99{
101
102 if( !aBoard )
103 return result;
104
105 // source nickname + item name, so same-name parts from different libraries stay split
106 using SOURCE_KEY = std::pair<wxString, wxString>;
107
108 std::map<SOURCE_KEY, FOOTPRINT*> defByKey;
109 std::map<wxString, std::vector<FOOTPRINT*>> defsByName;
110
111 for( const std::unique_ptr<FOOTPRINT>& def : aDefinitions )
112 {
113 wxString name = def->GetFPID().GetUniStringLibItemName();
114
115 if( name.IsEmpty() )
116 continue;
117
118 defByKey.emplace( SOURCE_KEY( def->GetFPID().GetUniStringLibNickname(), name ), def.get() );
119 defsByName[name].push_back( def.get() );
120 }
121
122 // definitions can carry a different nickname from the placed footprints, so a unique name still
123 // matches, but an ambiguous one must not
124 auto findDef = [&]( const wxString& aNick, const wxString& aName ) -> FOOTPRINT*
125 {
126 if( auto it = defByKey.find( SOURCE_KEY( aNick, aName ) ); it != defByKey.end() )
127 return it->second;
128
129 auto byName = defsByName.find( aName );
130
131 if( byName == defsByName.end() || byName->second.size() != 1 )
132 return nullptr;
133
134 return byName->second.front();
135 };
136
137 // preload source libs before membership queries
138 for( const wxString& nick : aSourceLibNicknames )
139 {
140 if( m_adapter.GetRow( nick ) )
141 m_adapter.LoadOne( nick );
142 }
143
144 std::set<wxString> provenance( aSourceLibNicknames.begin(), aSourceLibNicknames.end() );
145
146 // resolve one source lib, empty if none or ambiguous
147 auto resolveSource = [&]( const FOOTPRINT* aFp, const wxString& aName ) -> wxString
148 {
149 std::vector<wxString> candidates;
150 wxString ownNick = aFp->GetFPID().GetUniStringLibNickname();
151
152 if( !ownNick.IsEmpty() )
153 candidates.push_back( ownNick );
154
155 for( const wxString& nick : aSourceLibNicknames )
156 {
157 if( nick != ownNick )
158 candidates.push_back( nick );
159 }
160
161 std::vector<wxString> matches;
162
163 for( const wxString& nick : candidates )
164 {
165 if( !m_adapter.GetRow( nick ) )
166 continue;
167
168 // A nickname the importer emitted is not provenance. An unrelated library that
169 // happens to carry the name must not swallow the imported definition, so it takes the
170 // link only when it holds the same footprint.
171 if( provenance.count( nick ) )
172 {
173 if( !m_adapter.FootprintExists( nick, aName ) )
174 continue;
175 }
176 else
177 {
178 FOOTPRINT* def = findDef( ownNick, aName );
179
180 if( !def )
181 continue;
182
183 // one load answers both existence and equivalence, FootprintExists is itself a load
184 std::unique_ptr<FOOTPRINT> candidate( m_adapter.LoadFootprint( nick, aName, true ) );
185
186 if( !candidate || !sameInterface( *candidate, *def ) )
187 continue;
188 }
189
190 matches.push_back( nick );
191 }
192
193 return matches.size() == 1 ? matches.front() : wxString( wxEmptyString );
194 };
195
196 // per-instance target, empty target = cache-bound
197 std::map<SOURCE_KEY, wxString> targetByKey;
198 std::set<SOURCE_KEY> cacheKeys;
199 std::map<SOURCE_KEY, std::vector<FOOTPRINT*>> instancesByKey;
200
201 for( FOOTPRINT* fp : aBoard->Footprints() )
202 {
203 wxString name = fp->GetFPID().GetUniStringLibItemName();
204
205 if( name.IsEmpty() )
206 continue;
207
208 SOURCE_KEY key( fp->GetFPID().GetUniStringLibNickname(), name );
209
210 instancesByKey[key].push_back( fp );
211
212 if( targetByKey.count( key ) )
213 continue;
214
215 wxString sourceNick = resolveSource( fp, name );
216 targetByKey[key] = sourceNick;
217
218 if( sourceNick.IsEmpty() )
219 cacheKeys.insert( key );
220 }
221
222 // a .pretty holds one file per footprint, so the file name must be unique, and case-folded for
223 // a cache moved to Windows or macOS
224 std::set<wxString> takenFiles;
225
226 auto uniqueName = [&takenFiles]( const wxString& aName )
227 {
228 for( int suffix = 0; ; ++suffix )
229 {
230 wxString candidate = suffix ? wxString::Format( wxS( "%s_%d" ), aName, suffix ) : aName;
231 wxString fileName = candidate;
232
233 ReplaceIllegalFileNameChars( fileName, '_' );
234 fileName.MakeLower();
235
236 if( takenFiles.insert( fileName ).second )
237 return candidate;
238 }
239 };
240
241 // canonical def per cache key, fall back to unique placed instance if importer gave none
242 std::map<wxString, FOOTPRINT*> cacheDefs;
243 std::map<SOURCE_KEY, wxString> cacheNameByKey;
244 std::map<const FOOTPRINT*, wxString> cacheNameByDef;
245 std::vector<std::unique_ptr<FOOTPRINT>> placedDefs;
246 std::vector<wxString> renameReports;
247
248 for( const SOURCE_KEY& key : cacheKeys )
249 {
250 const wxString& name = key.second;
251 FOOTPRINT* def = findDef( key.first, name );
252
253 if( !def )
254 {
255 const std::vector<FOOTPRINT*>& instances = instancesByKey[key];
256
257 if( instances.empty() )
258 continue;
259
260 wxString firstSig = placedSignature( instances.front() );
261
262 for( auto it = instances.begin() + 1; it != instances.end(); ++it )
263 {
264 if( placedSignature( *it ) != firstSig )
265 {
266 m_reporter.Report( wxString::Format( _( "Imported footprint '%s' has "
267 "conflicting placed definitions; "
268 "keeping the first." ), name ),
270 break;
271 }
272 }
273
274 placedDefs.emplace_back( static_cast<FOOTPRINT*>( instances.front()->Clone() ) );
275 def = placedDefs.back().get();
276 }
277
278 // one definition serving several source libraries stays a single cache item
279 if( auto it = cacheNameByDef.find( def ); it != cacheNameByDef.end() )
280 {
281 cacheNameByKey[key] = it->second;
282 continue;
283 }
284
285 wxString cacheName = uniqueName( name );
286
287 cacheNameByDef[def] = cacheName;
288 cacheNameByKey[key] = cacheName;
289 cacheDefs[cacheName] = def;
290
291 if( cacheName != name )
292 {
293 renameReports.push_back(
294 wxString::Format( _( "Imported footprint '%s' from '%s' was renamed to '%s' "
295 "because another library supplies a different footprint "
296 "of that name." ), name, key.first, cacheName ) );
297 }
298 }
299
300 // write residuals to an atomic .pretty and register the row
301 if( !cacheDefs.empty() )
302 writeAndRegisterCache( aCacheNickname, cacheDefs, result );
303
304 // no rename happened if the cache did not publish
305 if( !result.m_cacheNickname.IsEmpty() )
306 {
307 for( const wxString& report : renameReports )
308 m_reporter.Report( report, RPT_SEVERITY_WARNING );
309 }
310
311 // re-point nicks to the resolved lib, cache-bound footprints also take their cache item name
312 for( FOOTPRINT* fp : aBoard->Footprints() )
313 {
314 LIB_ID fpid = fp->GetFPID();
315 wxString name = fpid.GetUniStringLibItemName();
316
317 if( name.IsEmpty() )
318 continue;
319
320 SOURCE_KEY key( fpid.GetUniStringLibNickname(), name );
321 auto it = targetByKey.find( key );
322
323 if( it == targetByKey.end() )
324 {
325 result.m_unresolved++;
326 continue;
327 }
328
329 // empty resolution = cache-bound, resolves only once the cache is published
330 if( it->second.IsEmpty() )
331 {
332 auto cacheName = cacheNameByKey.find( key );
333
334 if( result.m_cacheNickname.IsEmpty() || cacheName == cacheNameByKey.end() )
335 {
336 result.m_unresolved++;
337 continue;
338 }
339
340 fpid.SetLibNickname( aCacheNickname );
341 fpid.SetLibItemName( cacheName->second );
342 fp->SetFPID( fpid );
343 result.m_linkedToCache++;
344 }
345 else
346 {
347 fpid.SetLibNickname( it->second );
348 fp->SetFPID( fpid );
349 result.m_linkedToSource++;
350 }
351 }
352
353 return result;
354}
355
356
358 const wxString& aCacheNickname, const std::map<wxString, FOOTPRINT*>& aCacheDefs,
360{
361 wxFileName finalFn( m_projectPath, aCacheNickname, FILEEXT::KiCadFootprintLibPathExtension );
362 wxString finalPath = finalFn.GetFullPath();
363 wxString tempPath = finalPath + wxS( ".tmp" );
364
365 // a nickname the user already owns is never repurposed, whatever its row points at
366 LIBRARY_TABLE_ROW* existingRow = m_adapter.GetRow( aCacheNickname ).value_or( nullptr );
367
368 if( existingRow && !isManagedCache( existingRow ) )
369 {
370 m_reporter.Report( wxString::Format( _( "A footprint library named '%s' is already "
371 "registered; leaving imported footprints "
372 "unresolved." ), aCacheNickname ),
374 return;
375 }
376
378
379 if( !pi )
380 {
381 m_reporter.Report( _( "Cannot reconcile imported footprints: no KiCad footprint "
382 "writer." ), RPT_SEVERITY_ERROR );
383 return;
384 }
385
386 // best-effort cleanup, must not throw
387 auto safeDelete = [&pi]( const wxString& aPath )
388 {
389 try
390 {
391 pi->DeleteLibrary( aPath );
392 }
393 catch( const IO_ERROR& )
394 {
395 }
396 };
397
398 bool wrote = false;
399
400 try
401 {
402 if( wxDir::Exists( tempPath ) )
403 pi->DeleteLibrary( tempPath );
404
405 pi->CreateLibrary( tempPath );
406
407 // without this every save re-parses the whole library written so far
408 std::map<std::string, UTF8> properties { { "skip_cache_validation", "" } };
409
410 // the definitions are ours to consume and FootprintSave copies what it keeps
411 for( const auto& [name, def] : aCacheDefs )
412 {
413 LIB_ID id = def->GetFPID();
414
415 id.SetLibNickname( aCacheNickname );
416 id.SetLibItemName( name );
417 def->SetFPID( id );
418 def->SetReference( wxS( "REF**" ) );
419 pi->FootprintSave( tempPath, def, &properties );
420 }
421
422 wrote = true;
423 }
424 catch( const IO_ERROR& ioe )
425 {
426 m_reporter.Report( wxString::Format( _( "Error writing imported footprint cache "
427 "'%s': %s" ), aCacheNickname, ioe.What() ),
429 }
430
431 if( !wrote )
432 {
433 if( wxDir::Exists( tempPath ) )
434 safeDelete( tempPath );
435
436 return;
437 }
438
439 // publish temp->final, replace only a managed cache, never a user lib
440 if( wxDir::Exists( finalPath ) )
441 {
442 if( isManagedCache( existingRow ) )
443 {
444 safeDelete( finalPath );
445 }
446 else
447 {
448 m_reporter.Report( wxString::Format( _( "A library already exists at '%s'; leaving "
449 "imported footprints unresolved." ),
450 finalPath ),
452 safeDelete( tempPath );
453 return;
454 }
455 }
456
457 if( !wxRenameFile( tempPath, finalPath, false ) )
458 {
459 m_reporter.Report( wxString::Format( _( "Could not publish imported footprint cache to "
460 "'%s'." ), finalPath ),
462 safeDelete( tempPath );
463 return;
464 }
465
466 // only claim the cache when its table row is registered, else FPIDs re-point to a dead nickname
467 if( !registerCacheRow( aCacheNickname ) )
468 return;
469
470 aResult.m_cacheNickname = aCacheNickname;
471 aResult.m_savedToCache = static_cast<int>( aCacheDefs.size() );
472}
473
474
475bool FOOTPRINT_IMPORT_RECONCILER::registerCacheRow( const wxString& aCacheNickname )
476{
477 std::optional<LIBRARY_TABLE*> tableOpt = m_adapter.ProjectTable();
478
479 if( !tableOpt || !*tableOpt )
480 {
481 m_reporter.Report( _( "Cannot register imported footprint cache: no project library "
482 "table." ), RPT_SEVERITY_ERROR );
483 return false;
484 }
485
486 LIBRARY_TABLE* table = *tableOpt;
487 wxString cacheFile = aCacheNickname + wxS( "." )
489 wxString uri = wxS( "${KIPRJMOD}/" ) + cacheFile;
490 LIBRARY_TABLE_ROW* row = table->HasRow( aCacheNickname )
491 ? table->Row( aCacheNickname ).value_or( nullptr )
492 : &table->InsertRow();
493
494 if( !row )
495 return false;
496
497 row->SetNickname( aCacheNickname );
498 row->SetURI( uri );
499 row->SetType( wxS( "KiCad" ) );
502
503 // an unsaved row is gone on restart, so the cache cannot be claimed
504 if( !table->Save() )
505 {
506 m_reporter.Report( _( "Error saving project footprint library table; imported footprints "
507 "left unresolved." ), RPT_SEVERITY_ERROR );
508 return false;
509 }
510
511 // load the cache so membership and the updater resolve it
512 m_adapter.LoadOne( aCacheNickname );
513 return true;
514}
515
516
518ReconcileImportedFootprints( std::vector<std::unique_ptr<FOOTPRINT>> aDefinitions, BOARD& aBoard,
519 PROJECT& aProject, const wxString& aBoardPath,
520 const std::map<std::string, UTF8>* aProperties, REPORTER& aReporter )
521{
523
524 // an importer that publishes nothing still reconciles, the placed footprints are the fallback
526
527 if( !adapter )
528 return result;
529
530 // manager pre-commits the cache nickname + source libs; standalone import derives from filename
531 wxString cacheNick;
532 std::vector<wxString> sourceLibs;
533 IMPORT_PROJ_PROPS::ReadFootprintProps( aProperties, cacheNick, sourceLibs );
534
535 if( cacheNick.IsEmpty() )
536 cacheNick = IMPORT_PROJ_PROPS::MakeCacheNickname( wxFileName( aBoardPath ).GetName() );
537
538 FOOTPRINT_IMPORT_RECONCILER reconciler( *adapter, aProject.GetProjectPath(), aReporter );
539
540 // reconciliation failure must not abort the import
541 try
542 {
543 result = reconciler.Reconcile( &aBoard, std::move( aDefinitions ), cacheNick, sourceLibs );
544 }
545 catch( const IO_ERROR& ioe )
546 {
547 aReporter.Report( wxString::Format( _( "Could not reconcile imported footprint libraries: "
548 "%s" ), ioe.What() ), RPT_SEVERITY_ERROR );
549 }
550
551 return result;
552}
553
554
556ReconcileImportedFootprints( PCB_IO& aPlugin, BOARD& aBoard, PROJECT& aProject,
557 const wxString& aBoardPath,
558 const std::map<std::string, UTF8>* aProperties, REPORTER& aReporter )
559{
560 std::vector<std::unique_ptr<FOOTPRINT>> definitions;
561
562 try
563 {
564 for( FOOTPRINT* footprint : aPlugin.GetImportedCachedLibraryFootprints() )
565 definitions.emplace_back( footprint );
566 }
567 catch( const IO_ERROR& )
568 {
569 // importer retains no definitions, the placed footprints are the fallback
570 }
571
572 return ReconcileImportedFootprints( std::move( definitions ), aBoard, aProject, aBoardPath,
573 aProperties, aReporter );
574}
const char * name
BOX2< VECTOR2I > BOX2I
Definition box2.h:927
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:409
const FOOTPRINTS & Footprints() const
Definition board.h:463
constexpr size_type GetWidth() const
Definition box2.h:211
constexpr size_type GetHeight() const
Definition box2.h:212
Frame-independent, non-interactive service that reconciles the footprint-library references of a fres...
void writeAndRegisterCache(const wxString &aCacheNickname, const std::map< wxString, FOOTPRINT * > &aCacheDefs, FOOTPRINT_IMPORT_RECONCILE_RESULT &aResult)
Write the residual definitions into an atomically-published .pretty and register its row.
FOOTPRINT_IMPORT_RECONCILER(FOOTPRINT_LIBRARY_ADAPTER &aAdapter, const wxString &aProjectPath, REPORTER &aReporter=NULL_REPORTER::GetInstance())
FOOTPRINT_IMPORT_RECONCILE_RESULT Reconcile(BOARD *aBoard, std::vector< std::unique_ptr< FOOTPRINT > > aDefinitions, const wxString &aCacheNickname, const std::vector< wxString > &aSourceLibNicknames)
Reconcile aBoard against the importer definitions and the provenance source libraries.
FOOTPRINT_LIBRARY_ADAPTER & m_adapter
bool registerCacheRow(const wxString &aCacheNickname)
Insert or refresh the project footprint-library-table row for the generated cache.
An interface to the global shared library manager that is schematic-specific and linked to one projec...
std::deque< PAD * > & Pads()
Definition footprint.h:404
const LIB_ID & GetFPID() const
Definition footprint.h:473
DRAWINGS & GraphicalItems()
Definition footprint.h:407
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
void SetOptions(const wxString &aOptions)
void SetNickname(const wxString &aNickname)
void SetType(const wxString &aType)
std::map< std::string, UTF8 > GetOptionsMap() const
void SetURI(const wxString &aUri)
void SetScope(LIBRARY_TABLE_SCOPE aScope)
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:45
int SetLibItemName(const UTF8 &aLibItemName)
Override the library item name portion of the LIB_ID to aLibItemName.
Definition lib_id.cpp:124
int SetLibNickname(const UTF8 &aLibNickname)
Override the logical library name portion of the LIB_ID to aLibNickname.
Definition lib_id.cpp:113
const wxString GetUniStringLibItemName() const
Get strings for display messages in dialogs.
Definition lib_id.h:108
const wxString GetUniStringLibNickname() const
Definition lib_id.h:84
Definition pad.h:61
@ KICAD_SEXP
S-expression Pcbnew file format.
Definition pcb_io_mgr.h:54
static PCB_IO * FindPlugin(PCB_FILE_T aFileType)
Return a #PLUGIN which the caller can use to import, export, save, or load design documents.
A base class that BOARD loading and saving plugins should derive from.
Definition pcb_io.h:76
virtual std::vector< FOOTPRINT * > GetImportedCachedLibraryFootprints()
Return a container with the cached library footprints generated in the last call to Load.
Definition pcb_io.cpp:99
static FOOTPRINT_LIBRARY_ADAPTER * FootprintLibAdapter(PROJECT *aProject)
Container for project specific data.
Definition project.h:63
virtual const wxString GetProjectPath() const
Return the full path of the project.
Definition project.cpp:183
A pure virtual class used to derive REPORTER objects from.
Definition reporter.h:73
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition reporter.h:102
#define _(s)
FOOTPRINT_IMPORT_RECONCILE_RESULT ReconcileImportedFootprints(std::vector< std::unique_ptr< FOOTPRINT > > aDefinitions, BOARD &aBoard, PROJECT &aProject, const wxString &aBoardPath, const std::map< std::string, UTF8 > *aProperties, REPORTER &aReporter)
Reconcile aBoard against the definitions an importer retained while loading it.
static const std::string KiCadFootprintLibPathExtension
std::unique_ptr< T > IO_RELEASER
Helper to hold and release an IO_BASE object when exceptions are thrown.
Definition io_mgr.h:33
wxString ManagedCacheOption()
Options string identifying a library-table row as a generated import cache.
constexpr char MANAGED_CACHE_KEY[]
Library-table row option key marking a row as a generated import cache.
wxString MakeCacheNickname(const wxString &aStem)
Derive the generated footprint-cache nickname from a project or file stem.
void ReadFootprintProps(const std::map< std::string, UTF8 > *aProps, wxString &aCacheNickname, std::vector< wxString > &aSourceFpLibs)
Read the footprint-import coordination properties out of a properties map.
@ RPT_SEVERITY_WARNING
@ RPT_SEVERITY_ERROR
bool ReplaceIllegalFileNameChars(std::string &aName, int aReplaceChar)
Checks aName for illegal file name characters.
Outcome of a post-import footprint-library reconciliation pass.
int m_savedToCache
distinct definitions written into the cache library
wxString m_cacheNickname
nickname of the generated cache, empty if none written
wxString result
Test unit parsing edge cases and error handling.
Definition of file extensions used in Kicad.