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>
30#include <lib_id.h>
31#include <pad.h>
32#include <reporter.h>
34#include <io/io_mgr.h>
35#include <pcb_io/pcb_io.h>
36#include <pcb_io/pcb_io_mgr.h>
39
40
42{
43 static const wxString option = wxS( "kicad_import_cache=1" );
44 return option;
45}
46
47
49 const wxString& aProjectPath,
50 REPORTER* aReporter ) :
51 m_adapter( aAdapter ),
52 m_projectPath( aProjectPath ),
53 m_reporter( aReporter )
54{
55}
56
57
58namespace
59{
60void report( REPORTER* aReporter, const wxString& aMsg, SEVERITY aSeverity )
61{
62 if( aReporter )
63 aReporter->Report( aMsg, aSeverity );
64}
65
66
67// structural signature, flags same-name placed instances that differ
68wxString placedSignature( const FOOTPRINT* aFp )
69{
70 BOX2I bbox = aFp->GetBoundingBox( false );
71
72 return wxString::Format( wxS( "%zu:%zu:%d:%d" ), aFp->Pads().size(),
73 aFp->GraphicalItems().size(), bbox.GetWidth(), bbox.GetHeight() );
74}
75
76
77// reuse existing row/dir only if prior import-managed cache
78bool existingIsManagedCache( FOOTPRINT_LIBRARY_ADAPTER& aAdapter, const wxString& aNickname )
79{
80 std::optional<LIBRARY_TABLE_ROW*> row = aAdapter.GetRow( aNickname );
81
82 if( !row || !*row )
83 return false;
84
85 return ( *row )->GetOptionsMap().count( "kicad_import_cache" ) > 0;
86}
87}
88
89
92 std::vector<std::unique_ptr<FOOTPRINT>> aDefinitions,
93 const wxString& aCacheNickname,
94 const std::vector<wxString>& aSourceLibNicknames )
95{
97
98 if( !aBoard )
99 return result;
100
101 // index importer defs by FPID item name
102 std::map<wxString, FOOTPRINT*> defByName;
103
104 for( const std::unique_ptr<FOOTPRINT>& def : aDefinitions )
105 {
106 wxString name = def->GetFPID().GetUniStringLibItemName();
107
108 if( !name.IsEmpty() )
109 defByName.emplace( name, def.get() );
110 }
111
112 // preload source libs before membership queries
113 for( const wxString& nick : aSourceLibNicknames )
114 {
115 if( m_adapter.GetRow( nick ) )
116 m_adapter.LoadOne( nick );
117 }
118
119 // resolve one source lib, empty if none or ambiguous
120 auto resolveSource = [&]( const FOOTPRINT* aFp, const wxString& aName ) -> wxString
121 {
122 std::vector<wxString> candidates;
123 wxString ownNick = aFp->GetFPID().GetUniStringLibNickname();
124
125 if( !ownNick.IsEmpty() )
126 candidates.push_back( ownNick );
127
128 for( const wxString& nick : aSourceLibNicknames )
129 {
130 if( nick != ownNick )
131 candidates.push_back( nick );
132 }
133
134 std::vector<wxString> matches;
135
136 for( const wxString& nick : candidates )
137 {
138 if( m_adapter.GetRow( nick ) && m_adapter.FootprintExists( nick, aName ) )
139 matches.push_back( nick );
140 }
141
142 return matches.size() == 1 ? matches.front() : wxString( wxEmptyString );
143 };
144
145 // per-instance target keyed by nick+name, so same-name parts from different libs stay split
146 // empty target = cache-bound
147 std::map<wxString, wxString> targetByKey;
148 std::set<wxString> cacheNames;
149 std::map<wxString, std::vector<FOOTPRINT*>> instancesByName;
150
151 auto keyOf = []( const wxString& aNick, const wxString& aName )
152 {
153 return aNick + wxS( "\x1f" ) + aName;
154 };
155
156 for( FOOTPRINT* fp : aBoard->Footprints() )
157 {
158 wxString name = fp->GetFPID().GetUniStringLibItemName();
159
160 if( name.IsEmpty() )
161 continue;
162
163 instancesByName[name].push_back( fp );
164
165 wxString key = keyOf( fp->GetFPID().GetUniStringLibNickname(), name );
166
167 if( targetByKey.count( key ) )
168 continue;
169
170 wxString sourceNick = resolveSource( fp, name );
171 targetByKey[key] = sourceNick;
172
173 if( sourceNick.IsEmpty() )
174 cacheNames.insert( name );
175 }
176
177 // canonical def per cache name, fall back to unique placed instance if importer gave none
178 std::map<wxString, FOOTPRINT*> cacheDefs;
179 std::map<wxString, std::unique_ptr<FOOTPRINT>> placedDefs;
180
181 for( const wxString& name : cacheNames )
182 {
183 if( auto it = defByName.find( name ); it != defByName.end() )
184 {
185 cacheDefs[name] = it->second;
186 continue;
187 }
188
189 const std::vector<FOOTPRINT*>& instances = instancesByName[name];
190
191 if( instances.empty() )
192 continue;
193
194 wxString firstSig = placedSignature( instances.front() );
195
196 for( auto it = instances.begin() + 1; it != instances.end(); ++it )
197 {
198 if( placedSignature( *it ) != firstSig )
199 {
200 report( m_reporter,
201 wxString::Format( _( "Imported footprint '%s' has conflicting placed "
202 "definitions; keeping the first." ), name ),
204 }
205 }
206
207 placedDefs[name] =
208 std::unique_ptr<FOOTPRINT>( static_cast<FOOTPRINT*>( instances.front()->Clone() ) );
209 cacheDefs[name] = placedDefs[name].get();
210 }
211
212 // write residuals to an atomic .pretty and register the row
213 if( !cacheDefs.empty() )
214 writeAndRegisterCache( aCacheNickname, cacheDefs, result );
215
216 // re-point board FPID nicks to resolved lib, keep item name
217 for( FOOTPRINT* fp : aBoard->Footprints() )
218 {
219 LIB_ID fpid = fp->GetFPID();
220 wxString name = fpid.GetUniStringLibItemName();
221
222 if( name.IsEmpty() )
223 continue;
224
225 auto it = targetByKey.find( keyOf( fpid.GetUniStringLibNickname(), name ) );
226
227 if( it == targetByKey.end() )
228 {
229 result.m_unresolved++;
230 continue;
231 }
232
233 // empty resolution = cache-bound, resolves only once the cache is published
234 if( it->second.IsEmpty() )
235 {
236 if( result.m_cacheNickname.IsEmpty() )
237 {
238 result.m_unresolved++;
239 continue;
240 }
241
242 fpid.SetLibNickname( aCacheNickname );
243 fp->SetFPID( fpid );
244 result.m_linkedToCache++;
245 }
246 else
247 {
248 fpid.SetLibNickname( it->second );
249 fp->SetFPID( fpid );
250 result.m_linkedToSource++;
251 }
252 }
253
254 return result;
255}
256
257
259 const wxString& aCacheNickname, const std::map<wxString, FOOTPRINT*>& aCacheDefs,
261{
262 wxFileName finalFn( m_projectPath, aCacheNickname, FILEEXT::KiCadFootprintLibPathExtension );
263 wxString finalPath = finalFn.GetFullPath();
264 wxString tempPath = finalPath + wxS( ".tmp" );
265
267
268 if( !pi )
269 {
270 report( m_reporter, _( "Cannot reconcile imported footprints: no KiCad footprint writer." ),
272 return;
273 }
274
275 // best-effort cleanup, must not throw
276 auto safeDelete = [&pi]( const wxString& aPath )
277 {
278 try
279 {
280 pi->DeleteLibrary( aPath );
281 }
282 catch( const IO_ERROR& )
283 {
284 }
285 };
286
287 bool wrote = false;
288
289 try
290 {
291 if( wxDir::Exists( tempPath ) )
292 pi->DeleteLibrary( tempPath );
293
294 pi->CreateLibrary( tempPath );
295
296 for( const auto& [name, def] : aCacheDefs )
297 {
298 std::unique_ptr<FOOTPRINT> copy( static_cast<FOOTPRINT*>( def->Clone() ) );
299 LIB_ID id = copy->GetFPID();
300
301 id.SetLibNickname( aCacheNickname );
302 copy->SetFPID( id );
303 copy->SetReference( wxS( "REF**" ) );
304 pi->FootprintSave( tempPath, copy.get() );
305 }
306
307 wrote = true;
308 }
309 catch( const IO_ERROR& ioe )
310 {
311 report( m_reporter,
312 wxString::Format( _( "Error writing imported footprint cache '%s': %s" ),
313 aCacheNickname, ioe.What() ),
315 }
316
317 if( !wrote )
318 {
319 if( wxDir::Exists( tempPath ) )
320 safeDelete( tempPath );
321
322 return;
323 }
324
325 // publish temp->final, replace only a managed cache, never a user lib
326 if( wxDir::Exists( finalPath ) )
327 {
328 if( existingIsManagedCache( m_adapter, aCacheNickname ) )
329 {
330 safeDelete( finalPath );
331 }
332 else
333 {
334 report( m_reporter,
335 wxString::Format( _( "A library already exists at '%s'; leaving imported "
336 "footprints unresolved." ), finalPath ),
338 safeDelete( tempPath );
339 return;
340 }
341 }
342
343 if( !wxRenameFile( tempPath, finalPath, false ) )
344 {
345 report( m_reporter,
346 wxString::Format( _( "Could not publish imported footprint cache to '%s'." ),
347 finalPath ),
349 safeDelete( tempPath );
350 return;
351 }
352
353 // only claim the cache when its table row is registered, else FPIDs re-point to a dead nickname
354 if( !registerCacheRow( aCacheNickname ) )
355 return;
356
357 aResult.m_cacheNickname = aCacheNickname;
358 aResult.m_cacheLibraryPath = finalPath;
359 aResult.m_savedToCache = static_cast<int>( aCacheDefs.size() );
360}
361
362
363bool FOOTPRINT_IMPORT_RECONCILER::registerCacheRow( const wxString& aCacheNickname )
364{
365 std::optional<LIBRARY_TABLE*> tableOpt = m_adapter.ProjectTable();
366
367 if( !tableOpt || !*tableOpt )
368 {
369 report( m_reporter, _( "Cannot register imported footprint cache: no project library "
370 "table." ), RPT_SEVERITY_ERROR );
371 return false;
372 }
373
374 LIBRARY_TABLE* table = *tableOpt;
375 wxString cacheFile = aCacheNickname + wxS( "." )
377 wxString uri = wxS( "${KIPRJMOD}/" ) + cacheFile;
378 LIBRARY_TABLE_ROW* row = table->HasRow( aCacheNickname )
379 ? table->Row( aCacheNickname ).value_or( nullptr )
380 : &table->InsertRow();
381
382 if( !row )
383 return false;
384
385 row->SetNickname( aCacheNickname );
386 row->SetURI( uri );
387 row->SetType( wxS( "KiCad" ) );
390
391 if( !table->Save() )
392 {
393 report( m_reporter, _( "Error saving project footprint library table." ),
395 }
396
397 // load the cache so membership and the updater resolve it
398 m_adapter.LoadOne( aCacheNickname );
399 return true;
400}
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
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.
static const wxString & ManagedCacheOption()
Options string identifying a footprint-library table row as a generated import cache.
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.
FOOTPRINT_IMPORT_RECONCILER(FOOTPRINT_LIBRARY_ADAPTER &aAdapter, const wxString &aProjectPath, REPORTER *aReporter=nullptr)
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()
std::optional< LIBRARY_TABLE_ROW * > GetRow(const wxString &aNickname, LIBRARY_TABLE_SCOPE aScope=LIBRARY_TABLE_SCOPE::BOTH) const
Like LIBRARY_MANAGER::GetRow but filtered to the LIBRARY_TABLE_TYPE of this adapter.
void SetOptions(const wxString &aOptions)
void SetNickname(const wxString &aNickname)
void SetType(const wxString &aType)
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
@ 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 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)
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
SEVERITY
@ 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 m_cacheLibraryPath
absolute path to the generated .pretty, empty if none
std::vector< std::vector< std::string > > table
wxString result
Test unit parsing edge cases and error handling.
Definition of file extensions used in Kicad.