41#include <wx/mstream.h>
42#include <wx/tokenzr.h>
43#include <wx/wfstream.h>
44#include <wx/zipstrm.h>
53 void error(
const json::json_pointer& ptr,
const json& instance,
54 const std::string& message )
override
56 throw std::invalid_argument( std::string(
"At " ) + ptr.to_string() +
", value:\n"
57 + instance.dump() +
"\n" + message +
"\n" );
70 void SetTitle(
const wxString& aTitle )
override
76 void Report(
const wxString& aMessage )
override
78 m_report = wxString::Format( wxT(
": %s" ), aMessage );
98 std::function<
void(
int )> aAvailableUpdateCallback,
99 std::function<
void(
const wxString )> aStatusCallback ) :
101 m_availableUpdateCallback( aAvailableUpdateCallback ),
102 m_statusCallback( aStatusCallback )
109 schema_file.AppendDir( wxS(
"schemas" ) );
111 std::ifstream schema_stream( schema_file.GetFullPath().ToUTF8() );
112 nlohmann::json schema;
116 schema_stream >> schema;
119 catch( std::exception& e )
121 if( !schema_file.FileExists() )
123 wxLogError( wxString::Format(
_(
"schema file '%s' not found" ),
124 schema_file.GetFullPath() ) );
128 wxLogError( wxString::Format(
_(
"Error loading schema: %s" ), e.what() ) );
137 std::ifstream installed_stream( f.GetFullPath().ToUTF8() );
138 nlohmann::json installed;
142 installed_stream >> installed;
144 if( installed.contains(
"packages" ) && installed[
"packages"].is_array() )
146 for(
const auto& js_entry : installed[
"packages"] )
153 catch( std::exception& e )
155 wxLogError( wxString::Format(
_(
"Error loading installed packages list: %s" ),
169 wxDir package_dir( d.GetPath() );
171 if( !package_dir.IsOpened() )
175 bool more = package_dir.GetFirst( &subdir,
"", wxDIR_DIRS | wxDIR_HIDDEN );
179 wxString actual_package_id = subdir;
180 actual_package_id.Replace(
'_',
'.' );
185 wxFileName subdir_file( d.GetPath(), subdir );
189 int stat_code = wxStat( subdir_file.GetFullPath(), &stat );
209 more = package_dir.GetNext( &subdir );
216 [&]( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& entry )
218 preparePackage( entry.second.package );
227 auto it = env.find( wxT(
"KICAD7_3RD_PARTY" ) );
229 if( it != env.end() && !it->second.GetValue().IsEmpty() )
238 const size_t aSizeLimit )
240 bool size_exceeded =
false;
242 TRANSFER_CALLBACK callback = [&](
size_t dltotal,
size_t dlnow,
size_t ultotal,
size_t ulnow )
244 if( aSizeLimit > 0 && ( dltotal > aSizeLimit || dlnow > aSizeLimit ) )
246 size_exceeded =
true;
255 aReporter->
Report( wxString::Format(
_(
"Downloading %lld/%lld kB" ), dlnow / 1000,
268 curl.
SetURL( aUrl.ToUTF8().data() );
277 if( code != CURLE_OK )
281 if( code == CURLE_ABORTED_BY_CALLBACK && size_exceeded )
282 wxMessageBox(
_(
"Download is too large." ) );
283 else if( code != CURLE_ABORTED_BY_CALLBACK )
297 std::stringstream repository_stream;
299 aReporter->
SetTitle(
_(
"Fetching repository" ) );
304 wxLogError(
_(
"Unable to load repository url" ) );
309 nlohmann::json repository_json;
313 repository_stream >> repository_json;
315 ValidateJson( repository_json, nlohmann::json_uri(
"#/definitions/Repository" ) );
319 catch(
const std::exception& e )
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." ) );
336 const nlohmann::json_uri& aUri )
const
344 const std::optional<wxString>& aHash,
345 std::vector<PCM_PACKAGE>& aPackages,
348 std::stringstream packages_stream;
350 aReporter->
SetTitle(
_(
"Fetching repository packages" ) );
355 wxLogError(
_(
"Unable to load repository packages url." ) );
360 std::istringstream isstream( packages_stream.str() );
362 if( aHash && !
VerifyHash( isstream, *aHash ) )
365 wxLogError(
_(
"Packages hash doesn't match. Repository may be corrupted." ) );
372 nlohmann::json packages_json = nlohmann::json::parse( packages_stream.str() );
373 ValidateJson( packages_json, nlohmann::json_uri(
"#/definitions/PackageArray" ) );
375 aPackages = packages_json[
"packages"].get<std::vector<PCM_PACKAGE>>();
377 catch( std::exception& e )
381 wxLogError( wxString::Format(
_(
"Unable to parse packages metadata:\n\n%s" ),
394 std::vector<unsigned char> bytes( picosha2::k_digest_size );
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() );
400 return aHash.compare( hex_str ) == 0;
408 wxT(
"Repository is not cached." ) );
419 const auto repository_tuple =
421 [&aRepositoryId](
const std::tuple<wxString, wxString, wxString>& t )
423 return std::get<0>( t ) == aRepositoryId;
429 wxString url = std::get<2>( *repository_tuple );
434 std::shared_ptr<PROGRESS_REPORTER> reporter;
437 reporter = std::make_shared<WX_PROGRESS_REPORTER>(
m_dialog, wxEmptyString, 1 );
444 bool packages_cache_exists =
false;
448 repo_cache.AppendDir( wxT(
"pcm" ) );
449 repo_cache.AppendDir( aRepositoryId );
450 wxFileName packages_cache( repo_cache.GetPath(), wxT(
"packages.json" ) );
452 if( repo_cache.FileExists() && packages_cache.FileExists() )
454 std::ifstream repo_stream( repo_cache.GetFullPath().ToUTF8() );
464 wxLogError(
_(
"Failed to parse locally stored repository.json." ) );
471 std::ifstream packages_cache_stream( packages_cache.GetFullPath().ToUTF8() );
475 packages_cache_stream >> js;
476 saved_repo.
package_list = js[
"packages"].get<std::vector<PCM_PACKAGE>>();
478 for(
size_t i = 0; i < saved_repo.
package_list.size(); i++ )
486 packages_cache_exists =
true;
492 wxLogError(
_(
"Packages cache for current repository is corrupted, it will "
493 "be redownloaded." ) );
499 if( !packages_cache_exists )
508 for(
size_t i = 0; i < current_repo.
package_list.size(); i++ )
514 repo_cache.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
516 std::ofstream repo_cache_stream( repo_cache.GetFullPath().ToUTF8() );
517 repo_cache_stream << std::setw( 4 ) << nlohmann::json( current_repo ) << std::endl;
519 std::ofstream packages_cache_stream( packages_cache.GetFullPath().ToUTF8() );
521 js[
"packages"] = nlohmann::json( current_repo.
package_list );
522 packages_cache_stream << std::setw( 4 ) << js << std::endl;
532 wxFileName resource_file( repo_cache.GetPath(), wxT(
"resources.zip" ) );
536 if( resource_file.FileExists() )
537 mtime = wxFileModificationTime( resource_file.GetFullPath() );
541 std::ofstream resources_stream( resource_file.GetFullPath().ToUTF8(),
542 std::ios_base::binary );
544 reporter->SetTitle(
_(
"Downloading resources" ) );
550 resources_stream.close();
554 std::ifstream read_stream( resource_file.GetFullPath().ToUTF8(),
555 std::ios_base::binary );
564 wxLogError(
_(
"Resources file hash doesn't match and will not be used. "
565 "Repository may be corrupted." ) );
568 wxRemoveFile( resource_file.GetFullPath() );
574 wxRemoveFile( resource_file.GetFullPath() );
595 wxLogDebug(
"Invalid/Missing repository " + aRepositoryId );
599 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair :
m_installed )
611 std::optional<PACKAGE_VERSION> current_version;
613 auto current_version_it =
617 return version.version == entry.current_version;
621 current_version = *current_version_it;
631 return version.version == entry.current_version;
642 return a.parsed_version > b.parsed_version;
654 int epoch = 0, major = 0, minor = 0, patch = 0;
659 wxStringTokenizer version_tokenizer( ver.
version, wxT(
"." ) );
661 major = wxAtoi( version_tokenizer.GetNextToken() );
663 if( version_tokenizer.HasMoreTokens() )
664 minor = wxAtoi( version_tokenizer.GetNextToken() );
666 if( version_tokenizer.HasMoreTokens() )
667 patch = wxAtoi( version_tokenizer.GetNextToken() );
669 ver.
parsed_version = std::make_tuple( epoch, major, minor, patch );
674 auto parse_version_tuple =
675 [](
const wxString& version,
int deflt )
677 int ver_major = deflt;
678 int ver_minor = deflt;
679 int ver_patch = deflt;
681 wxStringTokenizer tokenizer( version, wxT(
"." ) );
683 ver_major = wxAtoi( tokenizer.GetNextToken() );
685 if( tokenizer.HasMoreTokens() )
686 ver_minor = wxAtoi( tokenizer.GetNextToken() );
688 if( tokenizer.HasMoreTokens() )
689 ver_patch = wxAtoi( tokenizer.GetNextToken() );
691 return std::tuple<int, int, int>( ver_major, ver_minor, ver_patch );
702 wxString platform = wxT(
"windows" );
705 wxString platform = wxT(
"macos" );
708 wxString platform = wxT(
"linux" );
723 return a.parsed_version > b.parsed_version;
728const std::vector<PCM_PACKAGE>&
731 static std::vector<PCM_PACKAGE>
empty{};
749 auto it = std::find_if( aRepositories.begin(), aRepositories.end(),
750 [&](
const auto& new_entry )
752 return new_entry.first == std::get<1>( entry );
755 if( it == aRepositories.end() )
764 for(
const std::pair<wxString, wxString>& repo : aRepositories )
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,
779 repo_cache.AppendDir( wxT(
"pcm" ) );
780 repo_cache.AppendDir( aRepositoryId );
782 if( repo_cache.DirExists() )
783 repo_cache.Rmdir( wxPATH_RMDIR_RECURSIVE );
788 const wxString& aRepositoryId )
806 if( !aRepositoryId.IsEmpty() )
830 const wxString& aPackageId )
867 return ver.compatible;
878 wxT(
"GetPackageUpdateVersion called on a not installed package" ) );
882 auto installed_ver_it = std::find_if(
886 return ver.version == entry.current_version;
890 wxT(
"Installed package version not found" ) );
892 auto ver_it = std::find_if( aPackage.
versions.begin(), aPackage.
versions.end(),
895 return ver.compatible
896 && installed_ver_it->status >= ver.status
897 && installed_ver_it->parsed_version < ver.parsed_version;
900 return ver_it == aPackage.
versions.end() ? wxString( wxT(
"" ) ) : ver_it->version;
905 return std::chrono::duration_cast<std::chrono::seconds>(
906 std::chrono::system_clock::now().time_since_epoch() ).count();
915 js[
"packages"] = nlohmann::json::array();
917 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair :
m_installed )
919 js[
"packages"].emplace_back( pair.second );
923 std::ofstream stream( f.GetFullPath().ToUTF8() );
925 stream << std::setw( 4 ) << js << std::endl;
927 catch( nlohmann::detail::exception& )
936 std::vector<PCM_INSTALLATION_ENTRY> v;
939 [&v](
const std::pair<const wxString, PCM_INSTALLATION_ENTRY>& entry )
941 v.push_back( entry.second );
944 std::sort( v.begin(), v.end(),
947 return ( a.install_timestamp < b.install_timestamp )
948 || ( a.install_timestamp == b.install_timestamp
949 && a.package.identifier < b.package.identifier );
960 wxT(
"Installed package not found." ) );
962 return m_installed.at( aPackageId ).current_version;
985 const wxString& aSearchTerm )
987 wxArrayString terms = wxStringTokenize( aSearchTerm.Lower(), wxS(
" " ), wxTOKEN_STRTOK );
990 const auto find_term_matches =
991 [&](
const wxString& str )
994 wxString lower = str.Lower();
996 for(
const wxString& term : terms )
998 if( lower.Find( term ) != wxNOT_FOUND )
1006 if( terms.size() == 1 && terms[0] == aPackage.
identifier )
1009 if( terms.size() == 1 && find_term_matches( aPackage.
identifier ) )
1013 rank += 500 * find_term_matches( aPackage.
name );
1016 for(
const std::string& tag : aPackage.
tags )
1017 rank += 100 * find_term_matches( wxString( tag ) );
1020 rank += 10 * find_term_matches( aPackage.
description );
1024 rank += find_term_matches( aPackage.
author.
name );
1027 rank += 3 * find_term_matches( aPackage.
maintainer->name );
1030 for(
const std::pair<const std::string, wxString>& entry : aPackage.
resources )
1032 rank += find_term_matches( entry.first );
1033 rank += find_term_matches( entry.second );
1037 if( terms.size() == 1 && terms[0] == aPackage.
license )
1044std::unordered_map<wxString, wxBitmap>
1047 std::unordered_map<wxString, wxBitmap> bitmaps;
1050 resources_file.AppendDir( wxT(
"pcm" ) );
1051 resources_file.AppendDir( aRepositoryId );
1053 if( !resources_file.FileExists() )
1056 wxFFileInputStream stream( resources_file.GetFullPath() );
1057 wxZipInputStream
zip( stream );
1059 if( !
zip.IsOk() ||
zip.GetTotalEntries() == 0 )
1062 for( wxArchiveEntry* entry =
zip.GetNextEntry(); entry; entry =
zip.GetNextEntry() )
1064 wxArrayString path_parts = wxSplit( entry->GetName(), wxFileName::GetPathSeparator(),
1067 if( path_parts.size() != 2 || path_parts[1] != wxT(
"icon.png" ) )
1072 wxMemoryInputStream image_stream(
zip, entry->GetSize() );
1073 wxImage
image( image_stream, wxBITMAP_TYPE_PNG );
1074 bitmaps.emplace( path_parts[0], wxBitmap(
image ) );
1079 wxLogTrace( wxT(
"Error loading png bitmap for entry %s from %s" ), entry->GetName(),
1080 resources_file.GetFullPath() );
1090 std::unordered_map<wxString, wxBitmap> bitmaps;
1093 resources_dir_fn.AppendDir( wxT(
"resources" ) );
1094 wxDir resources_dir( resources_dir_fn.GetPath() );
1096 if( !resources_dir.IsOpened() )
1100 bool more = resources_dir.GetFirst( &subdir, wxEmptyString, wxDIR_DIRS | wxDIR_HIDDEN );
1104 wxFileName icon( resources_dir_fn.GetPath(), wxT(
"icon.png" ) );
1105 icon.AppendDir( subdir );
1107 if( icon.FileExists() )
1109 wxString actual_package_id = subdir;
1110 actual_package_id.Replace(
'_',
'.' );
1114 wxBitmap bitmap( icon.GetFullPath(), wxBITMAP_TYPE_PNG );
1115 bitmaps.emplace( actual_package_id, bitmap );
1120 wxLogTrace( wxT(
"Error loading png bitmap from %s" ), icon.GetFullPath() );
1124 more = resources_dir.GetNext( &subdir );
1146 std::unordered_set<wxString> repo_ids;
1148 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair :
m_installed )
1150 if( !pair.second.pinned )
1151 repo_ids.insert( pair.second.repository_id );
1156 if( repo_ids.count( repository_id ) == 0 )
1159 CacheRepository( repository_id );
1161 if( m_statusReporter->IsCancelled() )
1169 int availableUpdateCount = 0;
1171 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair :
m_installed )
1173 PCM_INSTALLATION_ENTRY& entry = pair.second;
1175 if( m_repository_cache.find( entry.repository_id ) != m_repository_cache.end() )
1177 PCM_PACKAGE_STATE state = GetPackageState( entry.repository_id,
1178 entry.package.identifier );
1180 if( state == PPS_UPDATE_AVAILABLE && !entry.pinned )
1181 availableUpdateCount++;
1191 m_statusCallback( availableUpdateCount > 0 ?
_(
"Package updates are available" )
1192 :
_(
"No package updates available" ) );
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.
static wxString GetStockDataPath(bool aRespectRunFromBuildDir=true)
Gets the stock (install) data path, which is the base path for things like scripting,...
static wxString GetUserCachePath()
Gets the stock (install) 3d viewer plugins path.
time_t getCurrentTimestamp() const
const std::vector< PCM_PACKAGE > & GetRepositoryPackages(const wxString &aRepositoryId) const
Get the packages metadata from a previously cached repository.
void SetRepositoryList(const STRING_PAIR_LIST &aRepositories)
Set list of repositories.
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.
std::unordered_map< wxString, PCM_REPOSITORY > m_repository_cache
const PCM_REPOSITORY & getCachedRepository(const wxString &aRepositoryId) const
Get the cached repository metadata.
std::shared_ptr< STATUS_TEXT_REPORTER > m_statusReporter
static void preparePackage(PCM_PACKAGE &aPackage)
Parses version strings and calculates compatibility.
int GetPackageSearchRank(const PCM_PACKAGE &aPackage, const wxString &aSearchTerm)
Get the approximate measure of how much given package matches the search term.
std::function< void(const wxString)> m_statusCallback
void MarkUninstalled(const PCM_PACKAGE &aPackage)
Mark package as uninstalled.
bool CacheRepository(const wxString &aRepositoryId)
Cache specified repository packages and other metadata.
void SaveInstalledPackages()
Saves metadata of installed packages to disk.
~PLUGIN_CONTENT_MANAGER()
const std::vector< PCM_INSTALLATION_ENTRY > GetInstalledPackages() const
Get list of installed packages.
void SetPinned(const wxString &aPackageId, const bool aPinned)
Set the pinned status of a package.
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.
std::thread m_updateThread
PCM_PACKAGE_STATE GetPackageState(const wxString &aRepositoryId, const wxString &aPackageId)
Get current state of the package.
PLUGIN_CONTENT_MANAGER(std::function< void(int)> aAvailableUpdateCallback, std::function< void(const wxString)> aStatusCallback)
std::map< wxString, PCM_INSTALLATION_ENTRY > m_installed
void RunBackgroundUpdate()
Runs a background update thread that checks for new package versions.
std::unordered_map< wxString, wxBitmap > GetRepositoryPackageBitmaps(const wxString &aRepositoryId)
Get the icon bitmaps for repository packages.
const wxString GetPackageUpdateVersion(const PCM_PACKAGE &aPackage)
Get the preferred package update version or empty string if there is none.
void MarkInstalled(const PCM_PACKAGE &aPackage, const wxString &aVersion, const wxString &aRepositoryId)
Mark package as installed.
nlohmann::json_schema::json_validator m_schema_validator
std::unordered_map< wxString, wxBitmap > GetInstalledPackageBitmaps()
Get the icon bitmaps for installed packages.
const wxString & GetInstalledPackageVersion(const wxString &aPackageId) const
Get the current version of an installed package.
void updateInstalledPackagesMetadata(const wxString &aRepositoryId)
Updates metadata of installed packages from freshly fetched repo.
bool IsPackagePinned(const wxString &aPackageId) const
Returns pinned status of a package.
static const std::tuple< int, int, int > m_kicad_version
void StopBackgroundUpdate()
Interrupts and joins() the update thread.
STRING_TUPLE_LIST m_repository_list
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.
std::function< void(int)> m_availableUpdateCallback
bool FetchRepository(const wxString &aUrl, PCM_REPOSITORY &aRepository, PROGRESS_REPORTER *aReporter)
Fetches repository metadata from given url.
void DiscardRepositoryCache(const wxString &aRepositoryId)
Discard in-memory and on-disk cache of a repository.
bool VerifyHash(std::istream &aStream, const wxString &aHash) const
Verifies SHA256 hash of a binary stream.
void ReadEnvVar()
Stores 3rdparty path from environment variables.
This implements all the tricky bits for thread safety, but the GUI is left to derived classes.
std::atomic_bool m_cancelled
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.
const std::function< void(const wxString)> m_statusCallback
void SetTitle(const wxString &aTitle) override
Change the title displayed on the window caption.
STATUS_TEXT_REPORTER(std::function< void(const wxString)> aStatusCallback)
void error(const json::json_pointer &ptr, const json &instance, const std::string &message) override
static bool empty(const wxTextEntryBase *aCtrl)
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
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
KIWAY Kiway & Pgm(), KFCTL_STANDALONE
The global Program "get" accessor.
< Package version metadata Package metadata
PCM_PACKAGE_VERSION_STATUS status
std::optional< wxString > kicad_version_max
std::optional< int > version_epoch
std::vector< std::string > platforms
std::tuple< int, int, int, int > parsed_version
uint64_t install_timestamp
Repository reference to a resource.
wxString description_full
std::vector< std::string > tags
std::optional< PCM_CONTACT > maintainer
std::vector< PACKAGE_VERSION > versions
Package installation entry.
PCM_RESOURCE_REFERENCE packages
std::vector< PCM_PACKAGE > package_list
std::optional< PCM_RESOURCE_REFERENCE > resources
std::unordered_map< wxString, size_t > package_map
std::optional< wxString > sha256
uint64_t update_timestamp
#define FN_NORMALIZE_FLAGS
Default flags to pass to wxFileName::Normalize().