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
25#include <wx/dir.h>
26#include <wx/filename.h>
27
28#include <board.h>
29#include <footprint.h>
31#include <lib_id.h>
32#include <pad.h>
33#include <project.h>
34#include <project_pcb.h>
35#include <reporter.h>
37#include <io/io_mgr.h>
38#include <pcb_io/pcb_io.h>
39#include <pcb_io/pcb_io_mgr.h>
42
43
45 const wxString& aProjectPath,
46 REPORTER& aReporter ) :
47 m_adapter( aAdapter ),
48 m_projectPath( aProjectPath ),
49 m_reporter( aReporter )
50{
51}
52
53
54namespace
55{
56// structural signature, flags same-name placed instances that differ
57wxString placedSignature( const FOOTPRINT* aFp )
58{
59 BOX2I bbox = aFp->GetBoundingBox( false );
60
61 return wxString::Format( wxS( "%zu:%zu:%d:%d" ), aFp->Pads().size(),
62 aFp->GraphicalItems().size(), bbox.GetWidth(), bbox.GetHeight() );
63}
64
65
66std::multiset<wxString> padNumbers( const FOOTPRINT& aFp )
67{
68 std::multiset<wxString> numbers;
69
70 for( const PAD* pad : aFp.Pads() )
71 numbers.insert( pad->GetNumber() );
72
73 return numbers;
74}
75
76
77// two tools never draw a footprint identically, so equivalence is the pad set
78bool sameInterface( const FOOTPRINT& aLhs, const FOOTPRINT& aRhs )
79{
80 return padNumbers( aLhs ) == padNumbers( aRhs );
81}
82
83
84// reuse existing row/dir only if prior import-managed cache
85bool isManagedCache( const LIBRARY_TABLE_ROW* aRow )
86{
87 return aRow && aRow->GetOptionsMap().count( IMPORT_PROJ_PROPS::MANAGED_CACHE_KEY ) > 0;
88}
89}
90
91
94 std::vector<std::unique_ptr<FOOTPRINT>> aDefinitions,
95 const wxString& aCacheNickname,
96 const std::vector<wxString>& aSourceLibNicknames )
97{
99
100 if( !aBoard )
101 return result;
102
103 std::map<wxString, FOOTPRINT*> defByName;
104
105 for( const std::unique_ptr<FOOTPRINT>& def : aDefinitions )
106 {
107 wxString name = def->GetFPID().GetUniStringLibItemName();
108
109 if( !name.IsEmpty() )
110 defByName.emplace( name, def.get() );
111 }
112
113 // preload source libs before membership queries
114 for( const wxString& nick : aSourceLibNicknames )
115 {
116 if( m_adapter.GetRow( nick ) )
117 m_adapter.LoadOne( nick );
118 }
119
120 std::set<wxString> provenance( aSourceLibNicknames.begin(), aSourceLibNicknames.end() );
121
122 // resolve one source lib, empty if none or ambiguous
123 auto resolveSource = [&]( const FOOTPRINT* aFp, const wxString& aName ) -> wxString
124 {
125 std::vector<wxString> candidates;
126 wxString ownNick = aFp->GetFPID().GetUniStringLibNickname();
127
128 if( !ownNick.IsEmpty() )
129 candidates.push_back( ownNick );
130
131 for( const wxString& nick : aSourceLibNicknames )
132 {
133 if( nick != ownNick )
134 candidates.push_back( nick );
135 }
136
137 std::vector<wxString> matches;
138
139 for( const wxString& nick : candidates )
140 {
141 if( !m_adapter.GetRow( nick ) )
142 continue;
143
144 // A nickname the importer emitted is not provenance. An unrelated library that
145 // happens to carry the name must not swallow the imported definition, so it takes the
146 // link only when it holds the same footprint.
147 if( provenance.count( nick ) )
148 {
149 if( !m_adapter.FootprintExists( nick, aName ) )
150 continue;
151 }
152 else
153 {
154 auto def = defByName.find( aName );
155
156 if( def == defByName.end() )
157 continue;
158
159 // one load answers both existence and equivalence, FootprintExists is itself a load
160 std::unique_ptr<FOOTPRINT> candidate( m_adapter.LoadFootprint( nick, aName, true ) );
161
162 if( !candidate || !sameInterface( *candidate, *def->second ) )
163 continue;
164 }
165
166 matches.push_back( nick );
167 }
168
169 return matches.size() == 1 ? matches.front() : wxString( wxEmptyString );
170 };
171
172 // per-instance target keyed by nick+name, so same-name parts from different libs stay split
173 // empty target = cache-bound
174 std::map<wxString, wxString> targetByKey;
175 std::set<wxString> cacheNames;
176 std::map<wxString, std::vector<FOOTPRINT*>> instancesByName;
177
178 auto keyOf = []( const wxString& aNick, const wxString& aName )
179 {
180 return aNick + wxS( "\x1f" ) + aName;
181 };
182
183 for( FOOTPRINT* fp : aBoard->Footprints() )
184 {
185 wxString name = fp->GetFPID().GetUniStringLibItemName();
186
187 if( name.IsEmpty() )
188 continue;
189
190 instancesByName[name].push_back( fp );
191
192 wxString key = keyOf( fp->GetFPID().GetUniStringLibNickname(), name );
193
194 if( targetByKey.count( key ) )
195 continue;
196
197 wxString sourceNick = resolveSource( fp, name );
198 targetByKey[key] = sourceNick;
199
200 if( sourceNick.IsEmpty() )
201 cacheNames.insert( name );
202 }
203
204 // canonical def per cache name, fall back to unique placed instance if importer gave none
205 std::map<wxString, FOOTPRINT*> cacheDefs;
206 std::vector<std::unique_ptr<FOOTPRINT>> placedDefs;
207
208 for( const wxString& name : cacheNames )
209 {
210 if( auto it = defByName.find( name ); it != defByName.end() )
211 {
212 cacheDefs[name] = it->second;
213 continue;
214 }
215
216 const std::vector<FOOTPRINT*>& instances = instancesByName[name];
217
218 if( instances.empty() )
219 continue;
220
221 wxString firstSig = placedSignature( instances.front() );
222
223 for( auto it = instances.begin() + 1; it != instances.end(); ++it )
224 {
225 if( placedSignature( *it ) != firstSig )
226 {
227 m_reporter.Report( wxString::Format( _( "Imported footprint '%s' has conflicting "
228 "placed definitions; keeping the first." ),
229 name ),
231 break;
232 }
233 }
234
235 placedDefs.emplace_back( static_cast<FOOTPRINT*>( instances.front()->Clone() ) );
236 cacheDefs[name] = placedDefs.back().get();
237 }
238
239 // write residuals to an atomic .pretty and register the row
240 if( !cacheDefs.empty() )
241 writeAndRegisterCache( aCacheNickname, cacheDefs, result );
242
243 // re-point nicks to the resolved lib, keep the item name
244 for( FOOTPRINT* fp : aBoard->Footprints() )
245 {
246 LIB_ID fpid = fp->GetFPID();
247 wxString name = fpid.GetUniStringLibItemName();
248
249 if( name.IsEmpty() )
250 continue;
251
252 auto it = targetByKey.find( keyOf( fpid.GetUniStringLibNickname(), name ) );
253
254 if( it == targetByKey.end() )
255 {
256 result.m_unresolved++;
257 continue;
258 }
259
260 // empty resolution = cache-bound, resolves only once the cache is published
261 if( it->second.IsEmpty() )
262 {
263 if( result.m_cacheNickname.IsEmpty() )
264 {
265 result.m_unresolved++;
266 continue;
267 }
268
269 fpid.SetLibNickname( aCacheNickname );
270 fp->SetFPID( fpid );
271 result.m_linkedToCache++;
272 }
273 else
274 {
275 fpid.SetLibNickname( it->second );
276 fp->SetFPID( fpid );
277 result.m_linkedToSource++;
278 }
279 }
280
281 return result;
282}
283
284
286 const wxString& aCacheNickname, const std::map<wxString, FOOTPRINT*>& aCacheDefs,
288{
289 wxFileName finalFn( m_projectPath, aCacheNickname, FILEEXT::KiCadFootprintLibPathExtension );
290 wxString finalPath = finalFn.GetFullPath();
291 wxString tempPath = finalPath + wxS( ".tmp" );
292
293 // a nickname the user already owns is never repurposed, whatever its row points at
294 LIBRARY_TABLE_ROW* existingRow = m_adapter.GetRow( aCacheNickname ).value_or( nullptr );
295
296 if( existingRow && !isManagedCache( existingRow ) )
297 {
298 m_reporter.Report( wxString::Format( _( "A footprint library named '%s' is already "
299 "registered; leaving imported footprints "
300 "unresolved." ), aCacheNickname ),
302 return;
303 }
304
306
307 if( !pi )
308 {
309 m_reporter.Report( _( "Cannot reconcile imported footprints: no KiCad footprint "
310 "writer." ), RPT_SEVERITY_ERROR );
311 return;
312 }
313
314 // best-effort cleanup, must not throw
315 auto safeDelete = [&pi]( const wxString& aPath )
316 {
317 try
318 {
319 pi->DeleteLibrary( aPath );
320 }
321 catch( const IO_ERROR& )
322 {
323 }
324 };
325
326 bool wrote = false;
327
328 try
329 {
330 if( wxDir::Exists( tempPath ) )
331 pi->DeleteLibrary( tempPath );
332
333 pi->CreateLibrary( tempPath );
334
335 // without this every save re-parses the whole library written so far
336 std::map<std::string, UTF8> properties { { "skip_cache_validation", "" } };
337
338 // the definitions are ours to consume and FootprintSave copies what it keeps
339 for( const auto& [name, def] : aCacheDefs )
340 {
341 LIB_ID id = def->GetFPID();
342
343 id.SetLibNickname( aCacheNickname );
344 def->SetFPID( id );
345 def->SetReference( wxS( "REF**" ) );
346 pi->FootprintSave( tempPath, def, &properties );
347 }
348
349 wrote = true;
350 }
351 catch( const IO_ERROR& ioe )
352 {
353 m_reporter.Report( wxString::Format( _( "Error writing imported footprint cache "
354 "'%s': %s" ), aCacheNickname, ioe.What() ),
356 }
357
358 if( !wrote )
359 {
360 if( wxDir::Exists( tempPath ) )
361 safeDelete( tempPath );
362
363 return;
364 }
365
366 // publish temp->final, replace only a managed cache, never a user lib
367 if( wxDir::Exists( finalPath ) )
368 {
369 if( isManagedCache( existingRow ) )
370 {
371 safeDelete( finalPath );
372 }
373 else
374 {
375 m_reporter.Report( wxString::Format( _( "A library already exists at '%s'; leaving "
376 "imported footprints unresolved." ),
377 finalPath ),
379 safeDelete( tempPath );
380 return;
381 }
382 }
383
384 if( !wxRenameFile( tempPath, finalPath, false ) )
385 {
386 m_reporter.Report( wxString::Format( _( "Could not publish imported footprint cache to "
387 "'%s'." ), finalPath ),
389 safeDelete( tempPath );
390 return;
391 }
392
393 // only claim the cache when its table row is registered, else FPIDs re-point to a dead nickname
394 if( !registerCacheRow( aCacheNickname ) )
395 return;
396
397 aResult.m_cacheNickname = aCacheNickname;
398 aResult.m_savedToCache = static_cast<int>( aCacheDefs.size() );
399}
400
401
402bool FOOTPRINT_IMPORT_RECONCILER::registerCacheRow( const wxString& aCacheNickname )
403{
404 std::optional<LIBRARY_TABLE*> tableOpt = m_adapter.ProjectTable();
405
406 if( !tableOpt || !*tableOpt )
407 {
408 m_reporter.Report( _( "Cannot register imported footprint cache: no project library "
409 "table." ), RPT_SEVERITY_ERROR );
410 return false;
411 }
412
413 LIBRARY_TABLE* table = *tableOpt;
414 wxString cacheFile = aCacheNickname + wxS( "." )
416 wxString uri = wxS( "${KIPRJMOD}/" ) + cacheFile;
417 LIBRARY_TABLE_ROW* row = table->HasRow( aCacheNickname )
418 ? table->Row( aCacheNickname ).value_or( nullptr )
419 : &table->InsertRow();
420
421 if( !row )
422 return false;
423
424 row->SetNickname( aCacheNickname );
425 row->SetURI( uri );
426 row->SetType( wxS( "KiCad" ) );
429
430 // an unsaved row is gone on restart, so the cache cannot be claimed
431 if( !table->Save() )
432 {
433 m_reporter.Report( _( "Error saving project footprint library table; imported footprints "
434 "left unresolved." ), RPT_SEVERITY_ERROR );
435 return false;
436 }
437
438 // load the cache so membership and the updater resolve it
439 m_adapter.LoadOne( aCacheNickname );
440 return true;
441}
442
443
445ReconcileImportedFootprints( std::vector<std::unique_ptr<FOOTPRINT>> aDefinitions, BOARD& aBoard,
446 PROJECT& aProject, const wxString& aBoardPath,
447 const std::map<std::string, UTF8>* aProperties, REPORTER& aReporter )
448{
450
451 // an importer that publishes nothing still reconciles, the placed footprints are the fallback
453
454 if( !adapter )
455 return result;
456
457 // manager pre-commits the cache nickname + source libs; standalone import derives from filename
458 wxString cacheNick;
459 std::vector<wxString> sourceLibs;
460 IMPORT_PROJ_PROPS::ReadFootprintProps( aProperties, cacheNick, sourceLibs );
461
462 if( cacheNick.IsEmpty() )
463 cacheNick = IMPORT_PROJ_PROPS::MakeCacheNickname( wxFileName( aBoardPath ).GetName() );
464
465 FOOTPRINT_IMPORT_RECONCILER reconciler( *adapter, aProject.GetProjectPath(), aReporter );
466
467 // reconciliation failure must not abort the import
468 try
469 {
470 result = reconciler.Reconcile( &aBoard, std::move( aDefinitions ), cacheNick, sourceLibs );
471 }
472 catch( const IO_ERROR& ioe )
473 {
474 aReporter.Report( wxString::Format( _( "Could not reconcile imported footprint libraries: "
475 "%s" ), ioe.What() ), RPT_SEVERITY_ERROR );
476 }
477
478 return result;
479}
480
481
483ReconcileImportedFootprints( PCB_IO& aPlugin, BOARD& aBoard, PROJECT& aProject,
484 const wxString& aBoardPath,
485 const std::map<std::string, UTF8>* aProperties, REPORTER& aReporter )
486{
487 std::vector<std::unique_ptr<FOOTPRINT>> definitions;
488
489 try
490 {
491 for( FOOTPRINT* footprint : aPlugin.GetImportedCachedLibraryFootprints() )
492 definitions.emplace_back( footprint );
493 }
494 catch( const IO_ERROR& )
495 {
496 // importer retains no definitions, the placed footprints are the fallback
497 }
498
499 return ReconcileImportedFootprints( std::move( definitions ), aBoard, aProject, aBoardPath,
500 aProperties, aReporter );
501}
const char * name
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
const FOOTPRINTS & Footprints() const
Definition board.h:421
constexpr size_type GetWidth() const
Definition box2.h:210
constexpr size_type GetHeight() const
Definition box2.h:211
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:375
const LIB_ID & GetFPID() const
Definition footprint.h:444
DRAWINGS & GraphicalItems()
Definition footprint.h:378
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 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:75
virtual std::vector< FOOTPRINT * > GetImportedCachedLibraryFootprints()
Return a container with the cached library footprints generated in the last call to Load.
Definition pcb_io.cpp:77
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:72
virtual REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED)
Report a string with a given severity.
Definition reporter.h:101
#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
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.