KiCad PCB EDA Suite
Loading...
Searching...
No Matches
design_block_io.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 (C) 2024 Mike Williams <[email protected]>
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <common.h>
22#include <gestfich.h>
23#include <json_common.h>
24#include <i18n_utility.h>
25#include <kiplatform/io.h>
26#include <wx/dir.h>
27#include <wx/ffile.h>
28#include <wx/filename.h>
29#include <wx/log.h>
30#include <wx/translation.h>
31#include <wx/string.h>
32#include <wx/arrstr.h>
33#include <wx/datetime.h>
35#include <kiway_player.h>
36#include <design_block_io.h>
37#include <design_block.h>
38#include <ki_exception.h>
39#include <trace_helpers.h>
40#include <fstream>
43
45{
46 // This token is serialized into the design-block-lib-table file, so it must stay
47 // locale-independent. "KiCad" is a product name and is never translated.
48 switch( aFileType )
49 {
50 case KICAD_SEXP: return wxT( "KiCad" );
52 default: return wxString::Format( wxT( "UNKNOWN (%d)" ), aFileType );
53 }
54}
55
56
58DESIGN_BLOCK_IO_MGR::EnumFromStr( const wxString& aFileType )
59{
60 if( aFileType.CmpNoCase( wxT( "KiCad" ) ) == 0 )
62 else if( aFileType == LIBRARY_TABLE_ROW::TABLE_TYPE_NAME )
64 else if( aFileType == _( "KiCad" ) )
65 {
66 // Accept the legacy translated token so tables written by older releases under a
67 // locale that translated "KiCad" still load.
69 }
70
72}
73
74
76{
77 switch( aFileType )
78 {
79 case KICAD_SEXP: return new DESIGN_BLOCK_IO();
80 default: return nullptr;
81 }
82}
83
84
86DESIGN_BLOCK_IO_MGR::GuessPluginTypeFromLibPath( const wxString& aLibPath, int aCtl )
87{
89
90 if( parser.Parse( aLibPath.ToStdString() ).has_value() )
91 return NESTED_TABLE;
92
93 if( IO_RELEASER<DESIGN_BLOCK_IO>( FindPlugin( KICAD_SEXP ) )->CanReadLibrary( aLibPath )
94 && aCtl != KICTL_NONKICAD_ONLY )
95 {
96 return KICAD_SEXP;
97 }
98
100}
101
102
103bool DESIGN_BLOCK_IO_MGR::ConvertLibrary( std::map<std::string, UTF8>* aOldFileProps,
104 const wxString& aOldFilePath,
105 const wxString& aNewFilePath )
106{
109
110 if( oldFileType == DESIGN_BLOCK_IO_MGR::FILE_TYPE_NONE )
111 return false;
112
113 // A nested library table has no plugin to enumerate it; reject it before dereferencing
114 // the null plugin below.
115 if( oldFileType == DESIGN_BLOCK_IO_MGR::NESTED_TABLE )
116 return false;
117
120
121 if( !oldFilePI || !kicadPI )
122 return false;
123
124 wxArrayString dbNames;
125 wxFileName newFileName( aNewFilePath );
126
127 if( newFileName.HasExt() )
128 {
129 wxString extraDir = newFileName.GetFullName();
130 newFileName.ClearExt();
131 newFileName.SetName( "" );
132 newFileName.AppendDir( extraDir );
133 }
134
135 if( !newFileName.DirExists() && !wxFileName::Mkdir( aNewFilePath, wxS_DIR_DEFAULT ) )
136 return false;
137
138 try
139 {
140 bool bestEfforts = false; // throw on first error
141 oldFilePI->DesignBlockEnumerate( dbNames, aOldFilePath, bestEfforts, aOldFileProps );
142
143 for( const wxString& dbName : dbNames )
144 {
145 std::unique_ptr<const DESIGN_BLOCK> db( oldFilePI->GetEnumeratedDesignBlock( aOldFilePath, dbName,
146 aOldFileProps ) );
147 kicadPI->DesignBlockSave( aNewFilePath, db.get() );
148 }
149 }
150 catch( ... )
151 {
152 return false;
153 }
154
155 return true;
156}
157
158
160{
161 return IO_BASE::IO_FILE_DESC( _HKI( "KiCad Design Block folders" ), {},
163}
164
165
166long long DESIGN_BLOCK_IO::GetLibraryTimestamp( const wxString& aLibraryPath ) const
167{
168 wxDir libDir( aLibraryPath );
169
170 if( !libDir.IsOpened() )
171 return 0;
172
173 long long ts = 0;
174
175 wxString filename;
176 bool hasMoreFiles = libDir.GetFirst( &filename, wxEmptyString, wxDIR_DIRS );
177
178 while( hasMoreFiles )
179 {
180 wxFileName blockDir( aLibraryPath, filename );
181
182 // Check if the directory ends with ".kicad_block", if so hash all the files in it.
183 if( blockDir.GetFullName().EndsWith( FILEEXT::KiCadDesignBlockPathExtension ) )
184 ts += KIPLATFORM::IO::TimestampDir( blockDir.GetFullPath(), wxT( "*" ) );
185
186 hasMoreFiles = libDir.GetNext( &filename );
187 }
188
189 return ts;
190}
191
192
193void DESIGN_BLOCK_IO::CreateLibrary( const wxString& aLibraryPath,
194 const std::map<std::string, UTF8>* aProperties )
195{
196 if( wxDir::Exists( aLibraryPath ) )
197 {
198 THROW_IO_ERROR( wxString::Format( _( "Cannot overwrite library path '%s'." ),
199 aLibraryPath.GetData() ) );
200 }
201
202 wxFileName dir;
203 dir.SetPath( aLibraryPath );
204
205 if( !dir.Mkdir() )
206 {
208 wxString::Format( _( "Library path '%s' could not be created.\n\n"
209 "Make sure you have write permissions and try again." ),
210 dir.GetPath() ) );
211 }
212}
213
214
215bool DESIGN_BLOCK_IO::DeleteLibrary( const wxString& aLibraryPath,
216 const std::map<std::string, UTF8>* aProperties )
217{
218 wxFileName fn;
219 fn.SetPath( aLibraryPath );
220
221 // Return if there is no library path to delete.
222 if( !fn.DirExists() )
223 return false;
224
225 if( !fn.IsDirWritable() )
226 {
227 THROW_IO_ERROR( wxString::Format( _( "Insufficient permissions to delete folder '%s'." ),
228 aLibraryPath.GetData() ) );
229 }
230
231 wxDir dir( aLibraryPath );
232
233 // Design block folders should only contain sub-folders for each design block
234 if( dir.HasFiles() )
235 {
236 THROW_IO_ERROR( wxString::Format( _( "Library folder '%s' has unexpected files." ),
237 aLibraryPath.GetData() ) );
238 }
239
240 // Must delete all sub-directories before deleting the library directory
241 if( dir.HasSubDirs() )
242 {
243 wxArrayString dirs;
244
245 // Get all sub-directories in the library path
246 CollectSubdirsLoopSafe( aLibraryPath, dirs );
247
248 for( size_t i = 0; i < dirs.GetCount(); i++ )
249 {
250 wxFileName tmp = dirs[i];
251
253 {
254 THROW_IO_ERROR( wxString::Format( _( "Unexpected folder '%s' found in library path '%s'." ),
255 dirs[i].GetData(), aLibraryPath.GetData() ) );
256 }
257 }
258
259 for( size_t i = 0; i < dirs.GetCount(); i++ )
260 wxRemoveFile( dirs[i] );
261 }
262
263 wxLogTrace( traceDesignBlocks, wxT( "Removing design block library '%s'." ),
264 aLibraryPath.GetData() );
265
266 // Some of the more elaborate wxRemoveFile() crap puts up its own wxLog dialog
267 // we don't want that. we want bare metal portability with no UI here.
268 if( !wxFileName::Rmdir( aLibraryPath, wxPATH_RMDIR_RECURSIVE ) )
269 {
270 THROW_IO_ERROR( wxString::Format( _( "Design block library '%s' cannot be deleted." ),
271 aLibraryPath.GetData() ) );
272 }
273
274 // For some reason removing a directory in Windows is not immediately updated. This delay
275 // prevents an error when attempting to immediately recreate the same directory when over
276 // writing an existing library.
277#ifdef __WINDOWS__
278 wxMilliSleep( 250L );
279#endif
280
281 return true;
282}
283
284
285void DESIGN_BLOCK_IO::DesignBlockEnumerate( wxArrayString& aDesignBlockNames,
286 const wxString& aLibraryPath, bool aBestEfforts,
287 const std::map<std::string, UTF8>* aProperties )
288{
289 // From the starting directory, look for all directories ending in the .kicad_block extension
290 wxDir dir( aLibraryPath );
291
292 if( !dir.IsOpened() )
293 {
294 THROW_IO_ERROR( wxString::Format( _( "Design block '%s' does not exist." ), aLibraryPath ) );
295 }
296
297 wxString dirname;
298 wxString fileSpec = wxT( "*." ) + wxString( FILEEXT::KiCadDesignBlockPathExtension );
299 bool cont = dir.GetFirst( &dirname, fileSpec, wxDIR_DIRS );
300
301 while( cont )
302 {
303 aDesignBlockNames.Add( dirname.Before( wxT( '.' ) ) );
304 cont = dir.GetNext( &dirname );
305 }
306}
307
308
309DESIGN_BLOCK* DESIGN_BLOCK_IO::DesignBlockLoad( const wxString& aLibraryPath,
310 const wxString& aDesignBlockName, bool aKeepUUID,
311 const std::map<std::string, UTF8>* aProperties )
312{
313 wxString dbPath = aLibraryPath + wxFileName::GetPathSeparator() + aDesignBlockName + wxT( "." )
314 + FILEEXT::KiCadDesignBlockPathExtension + wxFileName::GetPathSeparator();
315 wxString dbSchPath = dbPath + aDesignBlockName + wxT( "." )
317 wxString dbPcbPath = dbPath + aDesignBlockName + wxT( "." ) + FILEEXT::KiCadPcbFileExtension;
318 wxString dbMetadataPath = dbPath + aDesignBlockName + wxT( "." ) + FILEEXT::JsonFileExtension;
319
320 if( !wxDir::Exists( dbPath ) )
321 THROW_IO_ERROR( wxString::Format( _( "Design block '%s' does not exist." ), dbPath ) );
322
323 DESIGN_BLOCK* newDB = new DESIGN_BLOCK();
324
325 // Library name needs to be empty for when we fill it in with the correct library nickname
326 // one layer above
327 newDB->SetLibId( LIB_ID( wxEmptyString, aDesignBlockName ) );
328
329 if( wxFileExists( dbSchPath ) )
330 newDB->SetSchematicFile( dbSchPath );
331
332 if( wxFileExists( dbPcbPath ) )
333 newDB->SetBoardFile( dbPcbPath );
334
335 // Parse the JSON file if it exists
336 if( wxFileExists( dbMetadataPath ) )
337 {
338 try
339 {
340 nlohmann::ordered_json dbMetadata;
341 std::ifstream dbMetadataFile( dbMetadataPath.fn_str() );
342
343 dbMetadataFile >> dbMetadata;
344
345 if( dbMetadata.contains( "description" ) )
346 newDB->SetLibDescription( dbMetadata["description"].get<std::string>() );
347
348 if( dbMetadata.contains( "keywords" ) )
349 newDB->SetKeywords( dbMetadata["keywords"].get<std::string>() );
350
351 // Read the "fields" object from the JSON
352 if( dbMetadata.contains( "fields" ) )
353 {
354 for( auto& item : dbMetadata["fields"].items() )
355 {
356 wxString name = wxString::FromUTF8( item.key() );
357 wxString value = wxString::FromUTF8( item.value().get<std::string>() );
358
359 newDB->GetFields()[name] = value;
360 }
361 }
362 }
363 catch( ... )
364 {
365 delete newDB;
366 THROW_IO_ERROR( wxString::Format( _( "Design block metadata file '%s' could not be read." ),
367 dbMetadataPath ) );
368 }
369 }
370
371 return newDB;
372}
373
374
375bool DESIGN_BLOCK_IO::DesignBlockExists( const wxString& aLibraryPath,
376 const wxString& aDesignBlockName,
377 const std::map<std::string, UTF8>* aProperties )
378{
379 wxString dbPath = aLibraryPath + wxFileName::GetPathSeparator() + aDesignBlockName + wxT( "." )
380 + FILEEXT::KiCadDesignBlockPathExtension + wxFileName::GetPathSeparator();
381
382 return wxDir::Exists( dbPath );
383}
384
385
386void DESIGN_BLOCK_IO::DesignBlockSave( const wxString& aLibraryPath,
387 const DESIGN_BLOCK* aDesignBlock,
388 const std::map<std::string, UTF8>* aProperties )
389{
390 // Make sure we have a valid LIB_ID or we can't save the design block
391 if( !aDesignBlock->GetLibId().IsValid() )
392 {
393 THROW_IO_ERROR( _( "Design block does not have a valid library ID." ) );
394 }
395
396 if( aDesignBlock->GetSchematicFile().IsEmpty() && aDesignBlock->GetBoardFile().IsEmpty() )
397 {
398 THROW_IO_ERROR( _( "Design block does not have a schematic or board file." ) );
399 }
400
401 wxFileName schematicFile( aDesignBlock->GetSchematicFile() );
402 wxFileName boardFile( aDesignBlock->GetBoardFile() );
403
404 if( !aDesignBlock->GetSchematicFile().IsEmpty() && !schematicFile.FileExists() )
405 {
407 wxString::Format( _( "Schematic source file '%s' does not exist." ), schematicFile.GetFullPath() ) );
408 }
409
410 if( !aDesignBlock->GetBoardFile().IsEmpty() && !boardFile.FileExists() )
411 {
412 THROW_IO_ERROR( wxString::Format( _( "Board source file '%s' does not exist." ), boardFile.GetFullPath() ) );
413 }
414
415 // Create the design block folder
416 wxFileName dbFolder( aLibraryPath + wxFileName::GetPathSeparator()
417 + aDesignBlock->GetLibId().GetLibItemName() + wxT( "." )
419 + wxFileName::GetPathSeparator() );
420
421 if( !dbFolder.DirExists() )
422 {
423 if( !dbFolder.Mkdir() )
424 {
425 THROW_IO_ERROR( wxString::Format( _( "Design block folder '%s' could not be created." ),
426 dbFolder.GetFullPath().GetData() ) );
427 }
428 }
429
430 if( !aDesignBlock->GetSchematicFile().IsEmpty() )
431 {
432 // The new schematic file name is based on the design block name, not the source sheet name
433 wxString dbSchematicFile = dbFolder.GetFullPath() + aDesignBlock->GetLibId().GetLibItemName() + wxT( "." )
435
436 // If the source and destination files are the same, then we don't need to copy the file
437 // as we are just updating the metadata
438 if( schematicFile.GetFullPath() != dbSchematicFile )
439 {
440 // Copy the source sheet file to the design block folder, under the design block name
441 if( !wxCopyFile( schematicFile.GetFullPath(), dbSchematicFile ) )
442 {
444 wxString::Format( _( "Schematic file '%s' could not be saved as design block at '%s'." ),
445 schematicFile.GetFullPath(), dbSchematicFile ) );
446 }
447 }
448 }
449
450 if( !aDesignBlock->GetBoardFile().IsEmpty() )
451 {
452 // The new Board file name is based on the design block name, not the source sheet name
453 wxString dbBoardFile = dbFolder.GetFullPath() + aDesignBlock->GetLibId().GetLibItemName() + wxT( "." )
455
456 // If the source and destination files are the same, then we don't need to copy the file
457 // as we are just updating the metadata
458 if( boardFile.GetFullPath() != dbBoardFile )
459 {
460 // Copy the source sheet file to the design block folder, under the design block name
461 if( !wxCopyFile( boardFile.GetFullPath(), dbBoardFile ) )
462 {
463 THROW_IO_ERROR( wxString::Format( _( "Board file '%s' could not be saved as design block at '%s'." ),
464 boardFile.GetFullPath(), dbBoardFile ) );
465 }
466 }
467 }
468
469 wxString dbMetadataFile = dbFolder.GetFullPath() + aDesignBlock->GetLibId().GetLibItemName()
470 + wxT( "." ) + FILEEXT::JsonFileExtension;
471
472 // Write the metadata file
473 nlohmann::ordered_json dbMetadata;
474 dbMetadata["description"] = aDesignBlock->GetLibDescription();
475 dbMetadata["keywords"] = aDesignBlock->GetKeywords();
476 dbMetadata["fields"] = aDesignBlock->GetFields();
477
478 bool success = false;
479
480 try
481 {
482 std::string payload = dbMetadata.dump( 0 );
483 wxString writeError;
484 success = KIPLATFORM::IO::AtomicWriteFile( dbMetadataFile, payload.data(), payload.size(),
485 &writeError );
486
487 if( !success )
488 {
489 wxLogError( _( "Cannot save design block metadata '%s': %s" ), dbMetadataFile,
490 writeError );
491 }
492 }
493 catch( ... )
494 {
495 success = false;
496 }
497
498 if( !success )
499 {
500 THROW_IO_ERROR( wxString::Format(
501 _( "Design block metadata file '%s' could not be saved." ), dbMetadataFile ) );
502 }
503}
504
505
506void DESIGN_BLOCK_IO::DesignBlockDelete( const wxString& aLibPath, const wxString& aDesignBlockName,
507 const std::map<std::string, UTF8>* aProperties )
508{
509 wxFileName dbDir = wxFileName( aLibPath + wxFileName::GetPathSeparator() + aDesignBlockName
511
512 if( !dbDir.DirExists() )
513 {
515 wxString::Format( _( "Design block '%s' does not exist." ), dbDir.GetFullName() ) );
516 }
517
518 // Delete the whole design block folder
519 if( !wxFileName::Rmdir( dbDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE ) )
520 {
521 THROW_IO_ERROR( wxString::Format( _( "Design block folder '%s' could not be deleted." ),
522 dbDir.GetFullPath().GetData() ) );
523 }
524}
525
526
527bool DESIGN_BLOCK_IO::IsLibraryWritable( const wxString& aLibraryPath )
528{
529 wxFileName path( aLibraryPath );
530 return path.IsOk() && path.IsDirWritable();
531}
const char * name
@ KICAD_SEXP
S-expression KiCad file format.
@ DESIGN_BLOCK_FILE_UNKNOWN
0 is not a legal menu id on Mac
static const wxString ShowType(DESIGN_BLOCK_FILE_T aFileType)
static DESIGN_BLOCK_FILE_T GuessPluginTypeFromLibPath(const wxString &aLibPath, int aCtl=0)
static DESIGN_BLOCK_FILE_T EnumFromStr(const wxString &aFileType)
static bool ConvertLibrary(std::map< std::string, UTF8 > *aOldFileProps, const wxString &aOldFilePath, const wxString &aNewFilePath)
Convert a design block library to the latest KiCad format.
static DESIGN_BLOCK_IO * FindPlugin(DESIGN_BLOCK_FILE_T aFileType)
bool DesignBlockExists(const wxString &aLibraryPath, const wxString &aDesignBlockName, const std::map< std::string, UTF8 > *aProperties=nullptr)
long long GetLibraryTimestamp(const wxString &aLibraryPath) const
void DesignBlockDelete(const wxString &aLibraryPath, const wxString &aDesignBlockName, const std::map< std::string, UTF8 > *aProperties=nullptr)
DESIGN_BLOCK * DesignBlockLoad(const wxString &aLibraryPath, const wxString &aDesignBlockName, bool aKeepUUID=false, const std::map< std::string, UTF8 > *aProperties=nullptr)
void DesignBlockSave(const wxString &aLibraryPath, const DESIGN_BLOCK *aDesignBlock, const std::map< std::string, UTF8 > *aProperties=nullptr)
void DesignBlockEnumerate(wxArrayString &aDesignBlockNames, const wxString &aLibraryPath, bool aBestEfforts, const std::map< std::string, UTF8 > *aProperties=nullptr)
virtual bool DeleteLibrary(const wxString &aLibraryPath, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Delete an existing library and returns true, or if library does not exist returns false,...
void CreateLibrary(const wxString &aLibraryPath, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Create a new empty library at aLibraryPath empty.
bool IsLibraryWritable(const wxString &aLibraryPath) override
Return true if the library at aLibraryPath is writable.
const IO_BASE::IO_FILE_DESC GetLibraryDesc() const override
Get the descriptor for the library container that this IO plugin operates on.
void SetLibDescription(const wxString &aDesc)
void SetKeywords(const wxString &aKeywords)
void SetSchematicFile(const wxString &aFile)
void SetBoardFile(const wxString &aFile)
const wxString & GetKeywords() const
const wxString & GetLibDescription() const
void SetLibId(const LIB_ID &aName)
const wxString & GetBoardFile() const
const wxString & GetSchematicFile() const
const LIB_ID & GetLibId() const
const nlohmann::ordered_map< wxString, wxString > & GetFields() const
tl::expected< LIBRARY_TABLE_IR, LIBRARY_PARSE_ERROR > Parse(const std::filesystem::path &aPath)
static const wxString TABLE_TYPE_NAME
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:45
bool IsValid() const
Check if this LID_ID is valid.
Definition lib_id.h:168
const UTF8 & GetLibItemName() const
Definition lib_id.h:98
The common library.
#define _(s)
void CollectSubdirsLoopSafe(const wxString &aRoot, wxArrayString &aDirs, int aFlags)
Recursively collect every subdirectory under aRoot using the same loop detection as CollectFilesLoopS...
Definition gestfich.cpp:827
static const std::string KiCadDesignBlockLibPathExtension
static const std::string KiCadDesignBlockPathExtension
static const std::string JsonFileExtension
static const std::string KiCadSchematicFileExtension
static const std::string KiCadPcbFileExtension
const wxChar *const traceDesignBlocks
Some functions to handle hotkeys in KiCad.
std::unique_ptr< T > IO_RELEASER
Helper to hold and release an IO_BASE object when exceptions are thrown.
Definition io_mgr.h:33
#define THROW_IO_ERROR(msg)
macro which captures the "call site" values of FILE_, __FUNCTION & LINE
#define KICTL_NONKICAD_ONLY
chosen file is non-KiCad according to user
bool AtomicWriteFile(const wxString &aTargetPath, const void *aData, size_t aSize, wxString *aError=nullptr)
Writes aData to aTargetPath via a sibling temp file, fsyncs the data and directory,...
long long TimestampDir(const wxString &aDirPath, const wxString &aFilespec)
Computes a hash of modification times and sizes for files matching a pattern.
Definition unix/io.cpp:123
#define _HKI(x)
Definition page_info.cpp:40
Container that describes file type info.
Definition io_base.h:43
std::string path
wxLogTrace helper definitions.
Definition of file extensions used in Kicad.