KiCad PCB EDA Suite
Loading...
Searching...
No Matches
pcm.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) 2021 Andrew Lutsenko, anlutsenko at gmail dot com
5 * Copyright (C) 1992-2022 KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21// kicad_curl.h *must be* included before any wxWidgets header to avoid conflicts
22// at least on Windows/msys2
25
26#include "core/wx_stl_compat.h"
27#include "build_version.h"
28#include "paths.h"
29#include "pcm.h"
30#include "pgm_base.h"
31#include "picosha2.h"
33#include <wx_filename.h>
34
35#include <fstream>
36#include <iomanip>
37#include <memory>
38#include <wx/dir.h>
39#include <wx/filefn.h>
40#include <wx/image.h>
41#include <wx/mstream.h>
42#include <wx/tokenzr.h>
43#include <wx/wfstream.h>
44#include <wx/zipstrm.h>
45
46
47const std::tuple<int, int, int> PLUGIN_CONTENT_MANAGER::m_kicad_version =
49
50
51class THROWING_ERROR_HANDLER : public nlohmann::json_schema::error_handler
52{
53 void error( const json::json_pointer& ptr, const json& instance,
54 const std::string& message ) override
55 {
56 throw std::invalid_argument( std::string( "At " ) + ptr.to_string() + ", value:\n"
57 + instance.dump() + "\n" + message + "\n" );
58 }
59};
60
61
63{
64public:
65 STATUS_TEXT_REPORTER( std::function<void( const wxString )> aStatusCallback ) :
66 PROGRESS_REPORTER_BASE( 1 ), m_statusCallback( aStatusCallback )
67 {
68 }
69
70 void SetTitle( const wxString& aTitle ) override
71 {
72 m_title = aTitle;
73 m_report = wxT( "" );
74 }
75
76 void Report( const wxString& aMessage ) override
77 {
78 m_report = wxString::Format( wxT( ": %s" ), aMessage );
79 }
80
81 void Cancel() { m_cancelled.store( true ); }
82
83private:
84 bool updateUI() override
85 {
86 m_statusCallback( wxString::Format( wxT( "%s%s" ), m_title, m_report ) );
87 return true;
88 }
89
90 const std::function<void( const wxString )> m_statusCallback;
91
92 wxString m_title;
93 wxString m_report;
94};
95
96
98 std::function<void( int )> aAvailableUpdateCallback,
99 std::function<void( const wxString )> aStatusCallback ) :
100 m_dialog( nullptr ),
101 m_availableUpdateCallback( aAvailableUpdateCallback ),
102 m_statusCallback( aStatusCallback )
103{
104 ReadEnvVar();
105
106 // Read and store pcm schema
107 wxFileName schema_file( PATHS::GetStockDataPath( true ), wxS( "pcm.v1.schema.json" ) );
108 schema_file.Normalize( FN_NORMALIZE_FLAGS | wxPATH_NORM_ENV_VARS );
109 schema_file.AppendDir( wxS( "schemas" ) );
110
111 std::ifstream schema_stream( schema_file.GetFullPath().ToUTF8() );
112 nlohmann::json schema;
113
114 try
115 {
116 schema_stream >> schema;
117 m_schema_validator.set_root_schema( schema );
118 }
119 catch( std::exception& e )
120 {
121 if( !schema_file.FileExists() )
122 {
123 wxLogError( wxString::Format( _( "schema file '%s' not found" ),
124 schema_file.GetFullPath() ) );
125 }
126 else
127 {
128 wxLogError( wxString::Format( _( "Error loading schema: %s" ), e.what() ) );
129 }
130 }
131
132 // Load currently installed packages
133 wxFileName f( SETTINGS_MANAGER::GetUserSettingsPath(), wxT( "installed_packages.json" ) );
134
135 if( f.FileExists() )
136 {
137 std::ifstream installed_stream( f.GetFullPath().ToUTF8() );
138 nlohmann::json installed;
139
140 try
141 {
142 installed_stream >> installed;
143
144 if( installed.contains( "packages" ) && installed["packages"].is_array() )
145 {
146 for( const auto& js_entry : installed["packages"] )
147 {
148 PCM_INSTALLATION_ENTRY entry = js_entry.get<PCM_INSTALLATION_ENTRY>();
149 m_installed.emplace( entry.package.identifier, entry );
150 }
151 }
152 }
153 catch( std::exception& e )
154 {
155 wxLogError( wxString::Format( _( "Error loading installed packages list: %s" ),
156 e.what() ) );
157 }
158 }
159
160 // As a fall back populate installed from names of directories
161
162 for( const wxString& dir : PCM_PACKAGE_DIRECTORIES )
163 {
164 wxFileName d( m_3rdparty_path, wxEmptyString );
165 d.AppendDir( dir );
166
167 if( d.DirExists() )
168 {
169 wxDir package_dir( d.GetPath() );
170
171 if( !package_dir.IsOpened() )
172 continue;
173
174 wxString subdir;
175 bool more = package_dir.GetFirst( &subdir, "", wxDIR_DIRS | wxDIR_HIDDEN );
176
177 while( more )
178 {
179 wxString actual_package_id = subdir;
180 actual_package_id.Replace( '_', '.' );
181
182 if( m_installed.find( actual_package_id ) == m_installed.end() )
183 {
185 wxFileName subdir_file( d.GetPath(), subdir );
186
187 // wxFileModificationTime bugs out on windows for directories
188 wxStructStat stat;
189 int stat_code = wxStat( subdir_file.GetFullPath(), &stat );
190
191 entry.package.name = subdir;
192 entry.package.identifier = actual_package_id;
193 entry.current_version = "0.0";
194 entry.repository_name = wxT( "<unknown>" );
195
196 if( stat_code == 0 )
197 entry.install_timestamp = stat.st_mtime;
198
199 PACKAGE_VERSION version;
200 version.version = "0.0";
201 version.status = PVS_STABLE;
203
204 entry.package.versions.emplace_back( version );
205
206 m_installed.emplace( actual_package_id, entry );
207 }
208
209 more = package_dir.GetNext( &subdir );
210 }
211 }
212 }
213
214 // Calculate package compatibility
215 std::for_each( m_installed.begin(), m_installed.end(),
216 [&]( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& entry )
217 {
218 preparePackage( entry.second.package );
219 } );
220}
221
222
224{
225 // Get 3rd party path
226 const ENV_VAR_MAP& env = Pgm().GetLocalEnvVariables();
227 auto it = env.find( wxT( "KICAD7_3RD_PARTY" ) );
228
229 if( it != env.end() && !it->second.GetValue().IsEmpty() )
230 m_3rdparty_path = it->second.GetValue();
231 else
233}
234
235
236bool PLUGIN_CONTENT_MANAGER::DownloadToStream( const wxString& aUrl, std::ostream* aOutput,
237 PROGRESS_REPORTER* aReporter,
238 const size_t aSizeLimit )
239{
240 bool size_exceeded = false;
241
242 TRANSFER_CALLBACK callback = [&]( size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow )
243 {
244 if( aSizeLimit > 0 && ( dltotal > aSizeLimit || dlnow > aSizeLimit ) )
245 {
246 size_exceeded = true;
247
248 // Non zero return means abort.
249 return true;
250 }
251
252 if( dltotal > 1000 )
253 {
254 aReporter->SetCurrentProgress( dlnow / (double) dltotal );
255 aReporter->Report( wxString::Format( _( "Downloading %lld/%lld kB" ), dlnow / 1000,
256 dltotal / 1000 ) );
257 }
258 else
259 {
260 aReporter->SetCurrentProgress( 0.0 );
261 }
262
263 return !aReporter->KeepRefreshing();
264 };
265
266 KICAD_CURL_EASY curl;
267 curl.SetOutputStream( aOutput );
268 curl.SetURL( aUrl.ToUTF8().data() );
269 curl.SetFollowRedirects( true );
270 curl.SetTransferCallback( callback, 250000L );
271
272 int code = curl.Perform();
273
274 if( !aReporter->IsCancelled() )
275 aReporter->SetCurrentProgress( 1.0 );
276
277 if( code != CURLE_OK )
278 {
279 if( m_dialog )
280 {
281 if( code == CURLE_ABORTED_BY_CALLBACK && size_exceeded )
282 wxMessageBox( _( "Download is too large." ) );
283 else if( code != CURLE_ABORTED_BY_CALLBACK )
284 wxLogError( wxString( curl.GetErrorText( code ) ) );
285 }
286
287 return false;
288 }
289
290 return true;
291}
292
293
294bool PLUGIN_CONTENT_MANAGER::FetchRepository( const wxString& aUrl, PCM_REPOSITORY& aRepository,
295 PROGRESS_REPORTER* aReporter )
296{
297 std::stringstream repository_stream;
298
299 aReporter->SetTitle( _( "Fetching repository" ) );
300
301 if( !DownloadToStream( aUrl, &repository_stream, aReporter, 20480 ) )
302 {
303 if( m_dialog )
304 wxLogError( _( "Unable to load repository url" ) );
305
306 return false;
307 }
308
309 nlohmann::json repository_json;
310
311 try
312 {
313 repository_stream >> repository_json;
314
315 ValidateJson( repository_json, nlohmann::json_uri( "#/definitions/Repository" ) );
316
317 aRepository = repository_json.get<PCM_REPOSITORY>();
318 }
319 catch( const std::exception& e )
320 {
321 if( m_dialog )
322 {
323 wxLogError( wxString::Format( _( "Unable to parse repository: %s" ), e.what() ) );
324 wxLogError( _( "The given repository URL does not look like a valid KiCad package "
325 "repository. Please double check the URL." ) );
326 }
327
328 return false;
329 }
330
331 return true;
332}
333
334
335void PLUGIN_CONTENT_MANAGER::ValidateJson( const nlohmann::json& aJson,
336 const nlohmann::json_uri& aUri ) const
337{
338 THROWING_ERROR_HANDLER error_handler;
339 m_schema_validator.validate( aJson, error_handler, aUri );
340}
341
342
343bool PLUGIN_CONTENT_MANAGER::fetchPackages( const wxString& aUrl,
344 const std::optional<wxString>& aHash,
345 std::vector<PCM_PACKAGE>& aPackages,
346 PROGRESS_REPORTER* aReporter )
347{
348 std::stringstream packages_stream;
349
350 aReporter->SetTitle( _( "Fetching repository packages" ) );
351
352 if( !DownloadToStream( aUrl, &packages_stream, aReporter ) )
353 {
354 if( m_dialog )
355 wxLogError( _( "Unable to load repository packages url." ) );
356
357 return false;
358 }
359
360 std::istringstream isstream( packages_stream.str() );
361
362 if( aHash && !VerifyHash( isstream, *aHash ) )
363 {
364 if( m_dialog )
365 wxLogError( _( "Packages hash doesn't match. Repository may be corrupted." ) );
366
367 return false;
368 }
369
370 try
371 {
372 nlohmann::json packages_json = nlohmann::json::parse( packages_stream.str() );
373 ValidateJson( packages_json, nlohmann::json_uri( "#/definitions/PackageArray" ) );
374
375 aPackages = packages_json["packages"].get<std::vector<PCM_PACKAGE>>();
376 }
377 catch( std::exception& e )
378 {
379 if( m_dialog )
380 {
381 wxLogError( wxString::Format( _( "Unable to parse packages metadata:\n\n%s" ),
382 e.what() ) );
383 }
384
385 return false;
386 }
387
388 return true;
389}
390
391
392bool PLUGIN_CONTENT_MANAGER::VerifyHash( std::istream& aStream, const wxString& aHash ) const
393{
394 std::vector<unsigned char> bytes( picosha2::k_digest_size );
395
396 picosha2::hash256( std::istreambuf_iterator<char>( aStream ), std::istreambuf_iterator<char>(),
397 bytes.begin(), bytes.end() );
398 std::string hex_str = picosha2::bytes_to_hex_string( bytes.begin(), bytes.end() );
399
400 return aHash.compare( hex_str ) == 0;
401}
402
403
404const PCM_REPOSITORY&
405PLUGIN_CONTENT_MANAGER::getCachedRepository( const wxString& aRepositoryId ) const
406{
407 wxASSERT_MSG( m_repository_cache.find( aRepositoryId ) != m_repository_cache.end(),
408 wxT( "Repository is not cached." ) );
409
410 return m_repository_cache.at( aRepositoryId );
411}
412
413
414bool PLUGIN_CONTENT_MANAGER::CacheRepository( const wxString& aRepositoryId )
415{
416 if( m_repository_cache.find( aRepositoryId ) != m_repository_cache.end() )
417 return true;
418
419 const auto repository_tuple =
420 std::find_if( m_repository_list.begin(), m_repository_list.end(),
421 [&aRepositoryId]( const std::tuple<wxString, wxString, wxString>& t )
422 {
423 return std::get<0>( t ) == aRepositoryId;
424 } );
425
426 if( repository_tuple == m_repository_list.end() )
427 return false;
428
429 wxString url = std::get<2>( *repository_tuple );
430
431 nlohmann::json js;
432 PCM_REPOSITORY current_repo;
433
434 std::shared_ptr<PROGRESS_REPORTER> reporter;
435
436 if( m_dialog )
437 reporter = std::make_shared<WX_PROGRESS_REPORTER>( m_dialog, wxEmptyString, 1 );
438 else
439 reporter = m_statusReporter;
440
441 if( !FetchRepository( url, current_repo, reporter.get() ) )
442 return false;
443
444 bool packages_cache_exists = false;
445
446 // First load repository data from local filesystem if available.
447 wxFileName repo_cache = wxFileName( PATHS::GetUserCachePath(), wxT( "repository.json" ) );
448 repo_cache.AppendDir( wxT( "pcm" ) );
449 repo_cache.AppendDir( aRepositoryId );
450 wxFileName packages_cache( repo_cache.GetPath(), wxT( "packages.json" ) );
451
452 if( repo_cache.FileExists() && packages_cache.FileExists() )
453 {
454 std::ifstream repo_stream( repo_cache.GetFullPath().ToUTF8() );
455 PCM_REPOSITORY saved_repo;
456 try
457 {
458 repo_stream >> js;
459 saved_repo = js.get<PCM_REPOSITORY>();
460 }
461 catch( ... )
462 {
463 if( m_dialog )
464 wxLogError( _( "Failed to parse locally stored repository.json." ) );
465 }
466
467 if( saved_repo.packages.update_timestamp == current_repo.packages.update_timestamp )
468 {
469 // Cached repo is up to date, use data on disk
470 js.clear();
471 std::ifstream packages_cache_stream( packages_cache.GetFullPath().ToUTF8() );
472
473 try
474 {
475 packages_cache_stream >> js;
476 saved_repo.package_list = js["packages"].get<std::vector<PCM_PACKAGE>>();
477
478 for( size_t i = 0; i < saved_repo.package_list.size(); i++ )
479 {
480 preparePackage( saved_repo.package_list[i] );
481 saved_repo.package_map[saved_repo.package_list[i].identifier] = i;
482 }
483
484 m_repository_cache[aRepositoryId] = std::move( saved_repo );
485
486 packages_cache_exists = true;
487 }
488 catch( ... )
489 {
490 if( m_dialog )
491 {
492 wxLogError( _( "Packages cache for current repository is corrupted, it will "
493 "be redownloaded." ) );
494 }
495 }
496 }
497 }
498
499 if( !packages_cache_exists )
500 {
501 // Cache doesn't exist or is out of date
502 if( !fetchPackages( current_repo.packages.url, current_repo.packages.sha256,
503 current_repo.package_list, reporter.get() ) )
504 {
505 return false;
506 }
507
508 for( size_t i = 0; i < current_repo.package_list.size(); i++ )
509 {
510 preparePackage( current_repo.package_list[i] );
511 current_repo.package_map[current_repo.package_list[i].identifier] = i;
512 }
513
514 repo_cache.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
515
516 std::ofstream repo_cache_stream( repo_cache.GetFullPath().ToUTF8() );
517 repo_cache_stream << std::setw( 4 ) << nlohmann::json( current_repo ) << std::endl;
518
519 std::ofstream packages_cache_stream( packages_cache.GetFullPath().ToUTF8() );
520 js.clear();
521 js["packages"] = nlohmann::json( current_repo.package_list );
522 packages_cache_stream << std::setw( 4 ) << js << std::endl;
523
524 m_repository_cache[aRepositoryId] = std::move( current_repo );
525 }
526
527 if( current_repo.resources )
528 {
529 // Check resources file date, redownload if needed
530 PCM_RESOURCE_REFERENCE& resources = *current_repo.resources;
531
532 wxFileName resource_file( repo_cache.GetPath(), wxT( "resources.zip" ) );
533
534 time_t mtime = 0;
535
536 if( resource_file.FileExists() )
537 mtime = wxFileModificationTime( resource_file.GetFullPath() );
538
539 if( mtime + 600 < getCurrentTimestamp() && mtime < (time_t) resources.update_timestamp )
540 {
541 std::ofstream resources_stream( resource_file.GetFullPath().ToUTF8(),
542 std::ios_base::binary );
543
544 reporter->SetTitle( _( "Downloading resources" ) );
545
546 // 100 Mb resource file limit
547 bool success = DownloadToStream( resources.url, &resources_stream, reporter.get(),
548 100 * 1024 * 1024 );
549
550 resources_stream.close();
551
552 if( success )
553 {
554 std::ifstream read_stream( resource_file.GetFullPath().ToUTF8(),
555 std::ios_base::binary );
556
557
558 if( resources.sha256 && !VerifyHash( read_stream, *resources.sha256 ) )
559 {
560 read_stream.close();
561
562 if( m_dialog )
563 {
564 wxLogError( _( "Resources file hash doesn't match and will not be used. "
565 "Repository may be corrupted." ) );
566 }
567
568 wxRemoveFile( resource_file.GetFullPath() );
569 }
570 }
571 else
572 {
573 // Not critical, just clean up the file
574 wxRemoveFile( resource_file.GetFullPath() );
575 }
576 }
577 }
578
579 updateInstalledPackagesMetadata( aRepositoryId );
580
581 return true;
582}
583
584
586{
587 const PCM_REPOSITORY* repository;
588
589 try
590 {
591 repository = &getCachedRepository( aRepositoryId );
592 }
593 catch( ... )
594 {
595 wxLogDebug( "Invalid/Missing repository " + aRepositoryId );
596 return;
597 }
598
599 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair : m_installed )
600 {
601 PCM_INSTALLATION_ENTRY& entry = pair.second;
602
603 // If current package is not from this repository, skip it
604 if( entry.repository_id != aRepositoryId )
605 continue;
606
607 // If current package is no longer in this repository, keep it as is
608 if( repository->package_map.count( entry.package.identifier ) == 0 )
609 continue;
610
611 std::optional<PACKAGE_VERSION> current_version;
612
613 auto current_version_it =
614 std::find_if( entry.package.versions.begin(), entry.package.versions.end(),
615 [&]( const PACKAGE_VERSION& version )
616 {
617 return version.version == entry.current_version;
618 } );
619
620 if( current_version_it != entry.package.versions.end() )
621 current_version = *current_version_it; // copy
622
623 // Copy repository metadata into installation entry
624 entry.package = repository->package_list[repository->package_map.at( entry.package.identifier )];
625
626 // Insert current version if it's missing from repository metadata
627 current_version_it =
628 std::find_if( entry.package.versions.begin(), entry.package.versions.end(),
629 [&]( const PACKAGE_VERSION& version )
630 {
631 return version.version == entry.current_version;
632 } );
633
634 if( current_version_it == entry.package.versions.end() )
635 {
636 entry.package.versions.emplace_back( *current_version );
637
638 // Re-sort the versions by descending version
639 std::sort( entry.package.versions.begin(), entry.package.versions.end(),
640 []( const PACKAGE_VERSION& a, const PACKAGE_VERSION& b )
641 {
642 return a.parsed_version > b.parsed_version;
643 } );
644 }
645 }
646}
647
648
650{
651 // Parse package version strings
652 for( PACKAGE_VERSION& ver : aPackage.versions )
653 {
654 int epoch = 0, major = 0, minor = 0, patch = 0;
655
656 if( ver.version_epoch )
657 epoch = *ver.version_epoch;
658
659 wxStringTokenizer version_tokenizer( ver.version, wxT( "." ) );
660
661 major = wxAtoi( version_tokenizer.GetNextToken() );
662
663 if( version_tokenizer.HasMoreTokens() )
664 minor = wxAtoi( version_tokenizer.GetNextToken() );
665
666 if( version_tokenizer.HasMoreTokens() )
667 patch = wxAtoi( version_tokenizer.GetNextToken() );
668
669 ver.parsed_version = std::make_tuple( epoch, major, minor, patch );
670
671 // Determine compatibility
672 ver.compatible = true;
673
674 auto parse_version_tuple =
675 []( const wxString& version, int deflt )
676 {
677 int ver_major = deflt;
678 int ver_minor = deflt;
679 int ver_patch = deflt;
680
681 wxStringTokenizer tokenizer( version, wxT( "." ) );
682
683 ver_major = wxAtoi( tokenizer.GetNextToken() );
684
685 if( tokenizer.HasMoreTokens() )
686 ver_minor = wxAtoi( tokenizer.GetNextToken() );
687
688 if( tokenizer.HasMoreTokens() )
689 ver_patch = wxAtoi( tokenizer.GetNextToken() );
690
691 return std::tuple<int, int, int>( ver_major, ver_minor, ver_patch );
692 };
693
694 if( parse_version_tuple( ver.kicad_version, 0 ) > m_kicad_version )
695 ver.compatible = false;
696
697 if( ver.kicad_version_max
698 && parse_version_tuple( *ver.kicad_version_max, 999 ) < m_kicad_version )
699 ver.compatible = false;
700
701#ifdef __WXMSW__
702 wxString platform = wxT( "windows" );
703#endif
704#ifdef __WXOSX__
705 wxString platform = wxT( "macos" );
706#endif
707#ifdef __WXGTK__
708 wxString platform = wxT( "linux" );
709#endif
710
711 if( ver.platforms.size() > 0
712 && std::find( ver.platforms.begin(), ver.platforms.end(), platform )
713 == ver.platforms.end() )
714 {
715 ver.compatible = false;
716 }
717 }
718
719 // Sort by descending version
720 std::sort( aPackage.versions.begin(), aPackage.versions.end(),
721 []( const PACKAGE_VERSION& a, const PACKAGE_VERSION& b )
722 {
723 return a.parsed_version > b.parsed_version;
724 } );
725}
726
727
728const std::vector<PCM_PACKAGE>&
729PLUGIN_CONTENT_MANAGER::GetRepositoryPackages( const wxString& aRepositoryId ) const
730{
731 static std::vector<PCM_PACKAGE> empty{};
732
733 try
734 {
735 return getCachedRepository( aRepositoryId ).package_list;
736 }
737 catch( ... )
738 {
739 return empty;
740 }
741}
742
743
745{
746 // Clean up cache folder if repository is not in new list
747 for( const std::tuple<wxString, wxString, wxString>& entry : m_repository_list )
748 {
749 auto it = std::find_if( aRepositories.begin(), aRepositories.end(),
750 [&]( const auto& new_entry )
751 {
752 return new_entry.first == std::get<1>( entry );
753 } );
754
755 if( it == aRepositories.end() )
756 {
757 DiscardRepositoryCache( std::get<0>( entry ) );
758 }
759 }
760
761 m_repository_list.clear();
762 m_repository_cache.clear();
763
764 for( const std::pair<wxString, wxString>& repo : aRepositories )
765 {
766 std::string url_sha = picosha2::hash256_hex_string( repo.second );
767 m_repository_list.push_back( std::make_tuple( url_sha.substr( 0, 16 ), repo.first,
768 repo.second ) );
769 }
770}
771
772
773void PLUGIN_CONTENT_MANAGER::DiscardRepositoryCache( const wxString& aRepositoryId )
774{
775 if( m_repository_cache.count( aRepositoryId ) > 0 )
776 m_repository_cache.erase( aRepositoryId );
777
778 wxFileName repo_cache = wxFileName( PATHS::GetUserCachePath(), "" );
779 repo_cache.AppendDir( wxT( "pcm" ) );
780 repo_cache.AppendDir( aRepositoryId );
781
782 if( repo_cache.DirExists() )
783 repo_cache.Rmdir( wxPATH_RMDIR_RECURSIVE );
784}
785
786
787void PLUGIN_CONTENT_MANAGER::MarkInstalled( const PCM_PACKAGE& aPackage, const wxString& aVersion,
788 const wxString& aRepositoryId )
789{
790 // In case of package update remove old data but keep pinned state
791 bool pinned = false;
792
793 if( m_installed.count( aPackage.identifier ) )
794 {
795 pinned = m_installed.at( aPackage.identifier ).pinned;
796 MarkUninstalled( aPackage );
797 }
798
800 entry.package = aPackage;
801 entry.current_version = aVersion;
802 entry.repository_id = aRepositoryId;
803
804 try
805 {
806 if( !aRepositoryId.IsEmpty() )
807 entry.repository_name = getCachedRepository( aRepositoryId ).name;
808 else
809 entry.repository_name = _( "Local file" );
810 }
811 catch( ... )
812 {
813 entry.repository_name = _( "Unknown" );
814 }
815
817 entry.pinned = pinned;
818
819 m_installed.emplace( aPackage.identifier, entry );
820}
821
822
824{
825 m_installed.erase( aPackage.identifier );
826}
827
828
830 const wxString& aPackageId )
831{
832 bool installed = m_installed.find( aPackageId ) != m_installed.end();
833
834 if( aRepositoryId.IsEmpty() || !CacheRepository( aRepositoryId ) )
835 return installed ? PPS_INSTALLED : PPS_UNAVAILABLE;
836
837 const PCM_REPOSITORY* repo;
838
839 try
840 {
841 repo = &getCachedRepository( aRepositoryId );
842 }
843 catch( ... )
844 {
845 return installed ? PPS_INSTALLED : PPS_UNAVAILABLE;
846 }
847
848 if( repo->package_map.count( aPackageId ) == 0 )
849 return installed ? PPS_INSTALLED : PPS_UNAVAILABLE;
850
851 const PCM_PACKAGE& pkg = repo->package_list[repo->package_map.at( aPackageId )];
852
853 if( installed )
854 {
855 // Package is installed, check for available updates at the same or
856 // higher (numerically lower) version stability level
857 wxString update_version = GetPackageUpdateVersion( pkg );
858
859 return update_version.IsEmpty() ? PPS_INSTALLED : PPS_UPDATE_AVAILABLE;
860 }
861 else
862 {
863 // Find any compatible version
864 auto ver_it = std::find_if( pkg.versions.begin(), pkg.versions.end(),
865 []( const PACKAGE_VERSION& ver )
866 {
867 return ver.compatible;
868 } );
869
870 return ver_it == pkg.versions.end() ? PPS_UNAVAILABLE : PPS_AVAILABLE;
871 }
872}
873
874
876{
877 wxASSERT_MSG( m_installed.find( aPackage.identifier ) != m_installed.end(),
878 wxT( "GetPackageUpdateVersion called on a not installed package" ) );
879
880 const PCM_INSTALLATION_ENTRY& entry = m_installed.at( aPackage.identifier );
881
882 auto installed_ver_it = std::find_if(
883 entry.package.versions.begin(), entry.package.versions.end(),
884 [&]( const PACKAGE_VERSION& ver )
885 {
886 return ver.version == entry.current_version;
887 } );
888
889 wxASSERT_MSG( installed_ver_it != entry.package.versions.end(),
890 wxT( "Installed package version not found" ) );
891
892 auto ver_it = std::find_if( aPackage.versions.begin(), aPackage.versions.end(),
893 [&]( const PACKAGE_VERSION& ver )
894 {
895 return ver.compatible
896 && installed_ver_it->status >= ver.status
897 && installed_ver_it->parsed_version < ver.parsed_version;
898 } );
899
900 return ver_it == aPackage.versions.end() ? wxString( wxT( "" ) ) : ver_it->version;
901}
902
904{
905 return std::chrono::duration_cast<std::chrono::seconds>(
906 std::chrono::system_clock::now().time_since_epoch() ).count();
907}
908
909
911{
912 try
913 {
914 nlohmann::json js;
915 js["packages"] = nlohmann::json::array();
916
917 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair : m_installed )
918 {
919 js["packages"].emplace_back( pair.second );
920 }
921
922 wxFileName f( SETTINGS_MANAGER::GetUserSettingsPath(), wxT( "installed_packages.json" ) );
923 std::ofstream stream( f.GetFullPath().ToUTF8() );
924
925 stream << std::setw( 4 ) << js << std::endl;
926 }
927 catch( nlohmann::detail::exception& )
928 {
929 // Ignore
930 }
931}
932
933
934const std::vector<PCM_INSTALLATION_ENTRY> PLUGIN_CONTENT_MANAGER::GetInstalledPackages() const
935{
936 std::vector<PCM_INSTALLATION_ENTRY> v;
937
938 std::for_each( m_installed.begin(), m_installed.end(),
939 [&v]( const std::pair<const wxString, PCM_INSTALLATION_ENTRY>& entry )
940 {
941 v.push_back( entry.second );
942 } );
943
944 std::sort( v.begin(), v.end(),
945 []( const PCM_INSTALLATION_ENTRY& a, const PCM_INSTALLATION_ENTRY& b )
946 {
947 return ( a.install_timestamp < b.install_timestamp )
948 || ( a.install_timestamp == b.install_timestamp
949 && a.package.identifier < b.package.identifier );
950 } );
951
952 return v;
953}
954
955
956const wxString&
957PLUGIN_CONTENT_MANAGER::GetInstalledPackageVersion( const wxString& aPackageId ) const
958{
959 wxASSERT_MSG( m_installed.find( aPackageId ) != m_installed.end(),
960 wxT( "Installed package not found." ) );
961
962 return m_installed.at( aPackageId ).current_version;
963}
964
965
966bool PLUGIN_CONTENT_MANAGER::IsPackagePinned( const wxString& aPackageId ) const
967{
968 if( m_installed.find( aPackageId ) == m_installed.end() )
969 return false;
970
971 return m_installed.at( aPackageId ).pinned;
972}
973
974
975void PLUGIN_CONTENT_MANAGER::SetPinned( const wxString& aPackageId, const bool aPinned )
976{
977 if( m_installed.find( aPackageId ) == m_installed.end() )
978 return;
979
980 m_installed.at( aPackageId ).pinned = aPinned;
981}
982
983
985 const wxString& aSearchTerm )
986{
987 wxArrayString terms = wxStringTokenize( aSearchTerm.Lower(), wxS( " " ), wxTOKEN_STRTOK );
988 int rank = 0;
989
990 const auto find_term_matches =
991 [&]( const wxString& str )
992 {
993 int result = 0;
994 wxString lower = str.Lower();
995
996 for( const wxString& term : terms )
997 {
998 if( lower.Find( term ) != wxNOT_FOUND )
999 result += 1;
1000 }
1001
1002 return result;
1003 };
1004
1005 // Match on package id
1006 if( terms.size() == 1 && terms[0] == aPackage.identifier )
1007 rank += 10000;
1008
1009 if( terms.size() == 1 && find_term_matches( aPackage.identifier ) )
1010 rank += 1000;
1011
1012 // Match on package name
1013 rank += 500 * find_term_matches( aPackage.name );
1014
1015 // Match on tags
1016 for( const std::string& tag : aPackage.tags )
1017 rank += 100 * find_term_matches( wxString( tag ) );
1018
1019 // Match on package description
1020 rank += 10 * find_term_matches( aPackage.description );
1021 rank += 10 * find_term_matches( aPackage.description_full );
1022
1023 // Match on author/maintainer
1024 rank += find_term_matches( aPackage.author.name );
1025
1026 if( aPackage.maintainer )
1027 rank += 3 * find_term_matches( aPackage.maintainer->name );
1028
1029 // Match on resources
1030 for( const std::pair<const std::string, wxString>& entry : aPackage.resources )
1031 {
1032 rank += find_term_matches( entry.first );
1033 rank += find_term_matches( entry.second );
1034 }
1035
1036 // Match on license
1037 if( terms.size() == 1 && terms[0] == aPackage.license )
1038 rank += 1;
1039
1040 return rank;
1041}
1042
1043
1044std::unordered_map<wxString, wxBitmap>
1046{
1047 std::unordered_map<wxString, wxBitmap> bitmaps;
1048
1049 wxFileName resources_file = wxFileName( PATHS::GetUserCachePath(), wxT( "resources.zip" ) );
1050 resources_file.AppendDir( wxT( "pcm" ) );
1051 resources_file.AppendDir( aRepositoryId );
1052
1053 if( !resources_file.FileExists() )
1054 return bitmaps;
1055
1056 wxFFileInputStream stream( resources_file.GetFullPath() );
1057 wxZipInputStream zip( stream );
1058
1059 if( !zip.IsOk() || zip.GetTotalEntries() == 0 )
1060 return bitmaps;
1061
1062 for( wxArchiveEntry* entry = zip.GetNextEntry(); entry; entry = zip.GetNextEntry() )
1063 {
1064 wxArrayString path_parts = wxSplit( entry->GetName(), wxFileName::GetPathSeparator(),
1065 (wxChar) 0 );
1066
1067 if( path_parts.size() != 2 || path_parts[1] != wxT( "icon.png" ) )
1068 continue;
1069
1070 try
1071 {
1072 wxMemoryInputStream image_stream( zip, entry->GetSize() );
1073 wxImage image( image_stream, wxBITMAP_TYPE_PNG );
1074 bitmaps.emplace( path_parts[0], wxBitmap( image ) );
1075 }
1076 catch( ... )
1077 {
1078 // Log and ignore
1079 wxLogTrace( wxT( "Error loading png bitmap for entry %s from %s" ), entry->GetName(),
1080 resources_file.GetFullPath() );
1081 }
1082 }
1083
1084 return bitmaps;
1085}
1086
1087
1088std::unordered_map<wxString, wxBitmap> PLUGIN_CONTENT_MANAGER::GetInstalledPackageBitmaps()
1089{
1090 std::unordered_map<wxString, wxBitmap> bitmaps;
1091
1092 wxFileName resources_dir_fn( m_3rdparty_path, wxEmptyString );
1093 resources_dir_fn.AppendDir( wxT( "resources" ) );
1094 wxDir resources_dir( resources_dir_fn.GetPath() );
1095
1096 if( !resources_dir.IsOpened() )
1097 return bitmaps;
1098
1099 wxString subdir;
1100 bool more = resources_dir.GetFirst( &subdir, wxEmptyString, wxDIR_DIRS | wxDIR_HIDDEN );
1101
1102 while( more )
1103 {
1104 wxFileName icon( resources_dir_fn.GetPath(), wxT( "icon.png" ) );
1105 icon.AppendDir( subdir );
1106
1107 if( icon.FileExists() )
1108 {
1109 wxString actual_package_id = subdir;
1110 actual_package_id.Replace( '_', '.' );
1111
1112 try
1113 {
1114 wxBitmap bitmap( icon.GetFullPath(), wxBITMAP_TYPE_PNG );
1115 bitmaps.emplace( actual_package_id, bitmap );
1116 }
1117 catch( ... )
1118 {
1119 // Log and ignore
1120 wxLogTrace( wxT( "Error loading png bitmap from %s" ), icon.GetFullPath() );
1121 }
1122 }
1123
1124 more = resources_dir.GetNext( &subdir );
1125 }
1126
1127 return bitmaps;
1128}
1129
1130
1132{
1133 // If the thread is already running don't create it again
1134 if( m_updateThread.joinable() )
1135 return;
1136
1137 m_statusReporter = std::make_shared<STATUS_TEXT_REPORTER>( m_statusCallback );
1138
1139 m_updateThread = std::thread(
1140 [this]()
1141 {
1142 if( m_installed.size() == 0 )
1143 return;
1144
1145 // Only fetch repositories that have installed not pinned packages
1146 std::unordered_set<wxString> repo_ids;
1147
1148 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair : m_installed )
1149 {
1150 if( !pair.second.pinned )
1151 repo_ids.insert( pair.second.repository_id );
1152 }
1153
1154 for( const auto& [ repository_id, name, url ] : m_repository_list )
1155 {
1156 if( repo_ids.count( repository_id ) == 0 )
1157 continue;
1158
1159 CacheRepository( repository_id );
1160
1161 if( m_statusReporter->IsCancelled() )
1162 break;
1163 }
1164
1165 if( m_statusReporter->IsCancelled() )
1166 return;
1167
1168 // Count packages with updates
1169 int availableUpdateCount = 0;
1170
1171 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair : m_installed )
1172 {
1173 PCM_INSTALLATION_ENTRY& entry = pair.second;
1174
1175 if( m_repository_cache.find( entry.repository_id ) != m_repository_cache.end() )
1176 {
1177 PCM_PACKAGE_STATE state = GetPackageState( entry.repository_id,
1178 entry.package.identifier );
1179
1180 if( state == PPS_UPDATE_AVAILABLE && !entry.pinned )
1181 availableUpdateCount++;
1182 }
1183
1184 if( m_statusReporter->IsCancelled() )
1185 return;
1186 }
1187
1188 // Update the badge on PCM button
1189 m_availableUpdateCallback( availableUpdateCount );
1190
1191 m_statusCallback( availableUpdateCount > 0 ? _( "Package updates are available" )
1192 : _( "No package updates available" ) );
1193 } );
1194}
1195
1196
1198{
1199 if( m_updateThread.joinable() )
1200 {
1201 m_statusReporter->Cancel();
1202 m_updateThread.join();
1203 }
1204}
1205
1206
1208{
1209 // By the time object is being destroyed the thread should be
1210 // stopped already but just in case do it here too.
1212}
const char * name
Definition: DXF_plotter.cpp:56
wxString GetMajorMinorVersion()
Get only the major and minor version in a string major.minor.
const std::tuple< int, int, int > & GetMajorMinorPatchTuple()
Get the build version numbers as a tuple.
int Perform()
Equivalent to curl_easy_perform.
bool SetTransferCallback(const TRANSFER_CALLBACK &aCallback, size_t aInterval)
bool SetURL(const std::string &aURL)
Set the request URL.
bool SetFollowRedirects(bool aFollow)
Enable the following of HTTP(s) and other redirects, by default curl does not follow redirects.
bool SetOutputStream(const std::ostream *aOutput)
const std::string GetErrorText(int aCode)
Fetch CURL's "friendly" error string for a given error code.
static wxString GetDefault3rdPartyPath()
Gets the default path for PCM packages.
Definition: paths.cpp:129
static wxString GetStockDataPath(bool aRespectRunFromBuildDir=true)
Gets the stock (install) data path, which is the base path for things like scripting,...
Definition: paths.cpp:150
static wxString GetUserCachePath()
Gets the stock (install) 3d viewer plugins path.
Definition: paths.cpp:321
time_t getCurrentTimestamp() const
Definition: pcm.cpp:903
const std::vector< PCM_PACKAGE > & GetRepositoryPackages(const wxString &aRepositoryId) const
Get the packages metadata from a previously cached repository.
Definition: pcm.cpp:729
void SetRepositoryList(const STRING_PAIR_LIST &aRepositories)
Set list of repositories.
Definition: pcm.cpp:744
void ValidateJson(const nlohmann::json &aJson, const nlohmann::json_uri &aUri=nlohmann::json_uri("#")) const
Validates json against a specific definition in the PCM schema.
Definition: pcm.cpp:335
wxWindow * m_dialog
Definition: pcm.h:393
std::unordered_map< wxString, PCM_REPOSITORY > m_repository_cache
Definition: pcm.h:397
const PCM_REPOSITORY & getCachedRepository(const wxString &aRepositoryId) const
Get the cached repository metadata.
Definition: pcm.cpp:405
std::shared_ptr< STATUS_TEXT_REPORTER > m_statusReporter
Definition: pcm.h:406
static void preparePackage(PCM_PACKAGE &aPackage)
Parses version strings and calculates compatibility.
Definition: pcm.cpp:649
int GetPackageSearchRank(const PCM_PACKAGE &aPackage, const wxString &aSearchTerm)
Get the approximate measure of how much given package matches the search term.
Definition: pcm.cpp:984
std::function< void(const wxString)> m_statusCallback
Definition: pcm.h:403
void MarkUninstalled(const PCM_PACKAGE &aPackage)
Mark package as uninstalled.
Definition: pcm.cpp:823
bool CacheRepository(const wxString &aRepositoryId)
Cache specified repository packages and other metadata.
Definition: pcm.cpp:414
void SaveInstalledPackages()
Saves metadata of installed packages to disk.
Definition: pcm.cpp:910
const std::vector< PCM_INSTALLATION_ENTRY > GetInstalledPackages() const
Get list of installed packages.
Definition: pcm.cpp:934
wxString m_3rdparty_path
Definition: pcm.h:395
void SetPinned(const wxString &aPackageId, const bool aPinned)
Set the pinned status of a package.
Definition: pcm.cpp:975
bool DownloadToStream(const wxString &aUrl, std::ostream *aOutput, PROGRESS_REPORTER *aReporter, const size_t aSizeLimit=DEFAULT_DOWNLOAD_MEM_LIMIT)
Downloads url to an output stream.
Definition: pcm.cpp:236
std::thread m_updateThread
Definition: pcm.h:404
PCM_PACKAGE_STATE GetPackageState(const wxString &aRepositoryId, const wxString &aPackageId)
Get current state of the package.
Definition: pcm.cpp:829
PLUGIN_CONTENT_MANAGER(std::function< void(int)> aAvailableUpdateCallback, std::function< void(const wxString)> aStatusCallback)
Definition: pcm.cpp:97
std::map< wxString, PCM_INSTALLATION_ENTRY > m_installed
Definition: pcm.h:400
void RunBackgroundUpdate()
Runs a background update thread that checks for new package versions.
Definition: pcm.cpp:1131
std::unordered_map< wxString, wxBitmap > GetRepositoryPackageBitmaps(const wxString &aRepositoryId)
Get the icon bitmaps for repository packages.
Definition: pcm.cpp:1045
const wxString GetPackageUpdateVersion(const PCM_PACKAGE &aPackage)
Get the preferred package update version or empty string if there is none.
Definition: pcm.cpp:875
void MarkInstalled(const PCM_PACKAGE &aPackage, const wxString &aVersion, const wxString &aRepositoryId)
Mark package as installed.
Definition: pcm.cpp:787
nlohmann::json_schema::json_validator m_schema_validator
Definition: pcm.h:394
std::unordered_map< wxString, wxBitmap > GetInstalledPackageBitmaps()
Get the icon bitmaps for installed packages.
Definition: pcm.cpp:1088
const wxString & GetInstalledPackageVersion(const wxString &aPackageId) const
Get the current version of an installed package.
Definition: pcm.cpp:957
void updateInstalledPackagesMetadata(const wxString &aRepositoryId)
Updates metadata of installed packages from freshly fetched repo.
Definition: pcm.cpp:585
bool IsPackagePinned(const wxString &aPackageId) const
Returns pinned status of a package.
Definition: pcm.cpp:966
static const std::tuple< int, int, int > m_kicad_version
Definition: pcm.h:401
void StopBackgroundUpdate()
Interrupts and joins() the update thread.
Definition: pcm.cpp:1197
STRING_TUPLE_LIST m_repository_list
Definition: pcm.h:398
bool fetchPackages(const wxString &aUrl, const std::optional< wxString > &aHash, std::vector< PCM_PACKAGE > &aPackages, PROGRESS_REPORTER *aReporter)
Downloads packages metadata to in memory stream, verifies hash and attempts to parse it.
Definition: pcm.cpp:343
std::function< void(int)> m_availableUpdateCallback
Definition: pcm.h:402
bool FetchRepository(const wxString &aUrl, PCM_REPOSITORY &aRepository, PROGRESS_REPORTER *aReporter)
Fetches repository metadata from given url.
Definition: pcm.cpp:294
void DiscardRepositoryCache(const wxString &aRepositoryId)
Discard in-memory and on-disk cache of a repository.
Definition: pcm.cpp:773
bool VerifyHash(std::istream &aStream, const wxString &aHash) const
Verifies SHA256 hash of a binary stream.
Definition: pcm.cpp:392
void ReadEnvVar()
Stores 3rdparty path from environment variables.
Definition: pcm.cpp:223
This implements all the tricky bits for thread safety, but the GUI is left to derived classes.
A progress reporter interface for use in multi-threaded environments.
virtual bool IsCancelled() const =0
virtual bool KeepRefreshing(bool aWait=false)=0
Update the UI (if any).
virtual void Report(const wxString &aMessage)=0
Display aMessage in the progress bar dialog.
virtual void SetTitle(const wxString &aTitle)=0
Change the title displayed on the window caption.
virtual void SetCurrentProgress(double aProgress)=0
Set the progress value to aProgress (0..1).
static wxString GetUserSettingsPath()
Return the user configuration path used to store KiCad's configuration files.
void Report(const wxString &aMessage) override
Display aMessage in the progress bar dialog.
Definition: pcm.cpp:76
const std::function< void(const wxString)> m_statusCallback
Definition: pcm.cpp:90
bool updateUI() override
Definition: pcm.cpp:84
wxString m_report
Definition: pcm.cpp:93
wxString m_title
Definition: pcm.cpp:92
void SetTitle(const wxString &aTitle) override
Change the title displayed on the window caption.
Definition: pcm.cpp:70
STATUS_TEXT_REPORTER(std::function< void(const wxString)> aStatusCallback)
Definition: pcm.cpp:65
void error(const json::json_pointer &ptr, const json &instance, const std::string &message) override
Definition: pcm.cpp:53
static bool empty(const wxTextEntryBase *aCtrl)
#define _(s)
nlohmann::json json
Definition: gerbview.cpp:44
std::map< wxString, ENV_VAR_ITEM > ENV_VAR_MAP
std::function< int(size_t, size_t, size_t, size_t)> TRANSFER_CALLBACK
Wrapper interface around the curl_easy API/.
std::vector< std::pair< wxString, wxString > > STRING_PAIR_LIST
Definition: pcm.h:75
PCM_PACKAGE_STATE
Definition: pcm.h:55
@ PPS_INSTALLED
Definition: pcm.h:58
@ PPS_UNAVAILABLE
Definition: pcm.h:57
@ PPS_UPDATE_AVAILABLE
Definition: pcm.h:61
@ PPS_AVAILABLE
Definition: pcm.h:56
const std::unordered_set< wxString > PCM_PACKAGE_DIRECTORIES({ "plugins", "footprints", "3dmodels", "symbols", "resources", "colors", })
< Contains list of all valid directories that get extracted from a package archive
@ PVS_STABLE
Definition: pcm_data.h:53
see class PGM_BASE
KIWAY Kiway & Pgm(), KFCTL_STANDALONE
The global Program "get" accessor.
Definition: single_top.cpp:115
< Package version metadata Package metadata
Definition: pcm_data.h:74
bool compatible
Definition: pcm_data.h:89
wxString version
Definition: pcm_data.h:75
PCM_PACKAGE_VERSION_STATUS status
Definition: pcm_data.h:81
std::optional< wxString > kicad_version_max
Definition: pcm_data.h:84
std::optional< int > version_epoch
Definition: pcm_data.h:76
std::vector< std::string > platforms
Definition: pcm_data.h:82
wxString kicad_version
Definition: pcm_data.h:83
std::tuple< int, int, int, int > parsed_version
Definition: pcm_data.h:88
wxString name
Definition: pcm_data.h:63
Definition: pcm_data.h:138
wxString repository_name
Definition: pcm_data.h:142
PCM_PACKAGE package
Definition: pcm_data.h:139
uint64_t install_timestamp
Definition: pcm_data.h:143
wxString repository_id
Definition: pcm_data.h:141
wxString current_version
Definition: pcm_data.h:140
bool pinned
Definition: pcm_data.h:144
Repository reference to a resource.
Definition: pcm_data.h:95
wxString description
Definition: pcm_data.h:97
wxString description_full
Definition: pcm_data.h:98
wxString identifier
Definition: pcm_data.h:99
wxString license
Definition: pcm_data.h:103
std::vector< std::string > tags
Definition: pcm_data.h:105
STRING_MAP resources
Definition: pcm_data.h:104
std::optional< PCM_CONTACT > maintainer
Definition: pcm_data.h:102
wxString name
Definition: pcm_data.h:96
std::vector< PACKAGE_VERSION > versions
Definition: pcm_data.h:107
PCM_CONTACT author
Definition: pcm_data.h:101
Package installation entry.
Definition: pcm_data.h:122
PCM_RESOURCE_REFERENCE packages
Definition: pcm_data.h:124
std::vector< PCM_PACKAGE > package_list
Definition: pcm_data.h:130
wxString name
Definition: pcm_data.h:123
std::optional< PCM_RESOURCE_REFERENCE > resources
Definition: pcm_data.h:125
std::unordered_map< wxString, size_t > package_map
Definition: pcm_data.h:132
Repository metadata.
Definition: pcm_data.h:113
std::optional< wxString > sha256
Definition: pcm_data.h:115
uint64_t update_timestamp
Definition: pcm_data.h:116
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().
Definition: wx_filename.h:38