43#include <wx/mstream.h>
44#include <wx/tokenzr.h>
45#include <wx/wfstream.h>
46#include <wx/zipstrm.h>
54static const wxChar
tracePcm[] = wxT(
"KICAD_PCM" );
63 void error(
const json::json_pointer& ptr,
const json& instance,
64 const std::string& message )
override
66 throw std::invalid_argument( std::string(
"At " ) + ptr.to_string() +
", value:\n"
67 + instance.dump() +
"\n" + message +
"\n" );
73 std::function<
void(
int )> aAvailableUpdateCallback ) :
75 m_availableUpdateCallback( aAvailableUpdateCallback )
82 schema_file.AppendDir( wxS(
"schemas" ) );
84 std::ifstream schema_stream( schema_file.GetFullPath().fn_str() );
85 nlohmann::json schema;
91 #if defined(__MINGW32__) && defined(_UCRT)
95 schema_stream >> schema;
98 catch( std::exception& e )
100 if( !schema_file.FileExists() )
102 wxLogError( wxString::Format(
_(
"schema file '%s' not found" ),
103 schema_file.GetFullPath() ) );
107 wxLogError( wxString::Format(
_(
"Error loading schema: %s" ), e.what() ) );
116 std::ifstream installed_stream( f.GetFullPath().fn_str() );
117 nlohmann::json installed;
121 installed_stream >> installed;
123 if( installed.contains(
"packages" ) && installed[
"packages"].is_array() )
125 for(
const auto& js_entry : installed[
"packages"] )
132 catch( std::exception& e )
134 wxLogError( wxString::Format(
_(
"Error loading installed packages list: %s" ),
148 wxDir package_dir( d.GetPath() );
150 if( !package_dir.IsOpened() )
154 bool more = package_dir.GetFirst( &subdir,
"", wxDIR_DIRS | wxDIR_HIDDEN );
158 wxString actual_package_id = subdir;
159 actual_package_id.Replace(
'_',
'.' );
164 wxFileName subdir_file( d.GetPath(), subdir );
168 int stat_code = wxStat( subdir_file.GetFullPath(), &stat );
188 more = package_dir.GetNext( &subdir );
195 [&]( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& entry )
197 PreparePackage( entry.second.package );
216 const size_t aSizeLimit )
218 bool size_exceeded =
false;
220 TRANSFER_CALLBACK callback = [&](
size_t dltotal,
size_t dlnow,
size_t ultotal,
size_t ulnow )
222 if( aSizeLimit > 0 && ( dltotal > aSizeLimit || dlnow > aSizeLimit ) )
224 size_exceeded =
true;
233 aReporter->
Report( wxString::Format(
_(
"Downloading %lld/%lld kB" ), dlnow / 1000,
246 curl.
SetURL( aUrl.ToUTF8().data() );
255 if( code != CURLE_OK )
259 if( code == CURLE_ABORTED_BY_CALLBACK && size_exceeded )
260 wxMessageBox(
_(
"Download is too large." ) );
261 else if( code != CURLE_ABORTED_BY_CALLBACK )
275 std::stringstream repository_stream;
277 aReporter->
SetTitle(
_(
"Fetching repository" ) );
282 wxLogError(
_(
"Unable to load repository url" ) );
287 nlohmann::json repository_json;
291 repository_stream >> repository_json;
293 ValidateJson( repository_json, nlohmann::json_uri(
"#/definitions/Repository" ) );
297 catch(
const std::exception& e )
301 wxLogError( wxString::Format(
_(
"Unable to parse repository: %s" ), e.what() ) );
302 wxLogError(
_(
"The given repository URL does not look like a valid KiCad package "
303 "repository. Please double check the URL." ) );
314 const nlohmann::json_uri& aUri )
const
322 const std::optional<wxString>& aHash,
323 std::vector<PCM_PACKAGE>& aPackages,
326 std::stringstream packages_stream;
328 aReporter->
SetTitle(
_(
"Fetching repository packages" ) );
333 wxLogError(
_(
"Unable to load repository packages url." ) );
338 std::istringstream isstream( packages_stream.str() );
340 if( aHash && !
VerifyHash( isstream, *aHash ) )
343 wxLogError(
_(
"Packages hash doesn't match. Repository may be corrupted." ) );
350 nlohmann::json packages_json = nlohmann::json::parse( packages_stream.str() );
351 ValidateJson( packages_json, nlohmann::json_uri(
"#/definitions/PackageArray" ) );
353 aPackages = packages_json[
"packages"].get<std::vector<PCM_PACKAGE>>();
355 catch( std::exception& e )
359 wxLogError( wxString::Format(
_(
"Unable to parse packages metadata:\n\n%s" ),
372 std::vector<unsigned char> bytes( picosha2::k_digest_size );
374 picosha2::hash256( std::istreambuf_iterator<char>( aStream ), std::istreambuf_iterator<char>(),
375 bytes.begin(), bytes.end() );
376 std::string hex_str = picosha2::bytes_to_hex_string( bytes.begin(), bytes.end() );
378 return aHash.compare( hex_str ) == 0;
386 wxT(
"Repository is not cached." ) );
397 const auto repository_tuple =
399 [&aRepositoryId](
const std::tuple<wxString, wxString, wxString>& t )
401 return std::get<0>( t ) == aRepositoryId;
407 wxString url = std::get<2>( *repository_tuple );
413 std::shared_ptr<PROGRESS_REPORTER> reporter;
416 reporter = std::make_shared<WX_PROGRESS_REPORTER>(
m_dialog, wxEmptyString, 1 );
423 bool packages_cache_exists =
false;
427 repo_cache.AppendDir( wxT(
"pcm" ) );
428 repo_cache.AppendDir( aRepositoryId );
429 wxFileName packages_cache( repo_cache.GetPath(), wxT(
"packages.json" ) );
431 if( repo_cache.FileExists() && packages_cache.FileExists() )
433 std::ifstream repo_stream( repo_cache.GetFullPath().fn_str() );
443 wxLogError(
_(
"Failed to parse locally stored repository.json." ) );
450 std::ifstream packages_cache_stream( packages_cache.GetFullPath().fn_str() );
454 packages_cache_stream >> js;
455 saved_repo.
package_list = js[
"packages"].get<std::vector<PCM_PACKAGE>>();
457 for(
size_t i = 0; i < saved_repo.
package_list.size(); i++ )
465 packages_cache_exists =
true;
471 wxLogError(
_(
"Packages cache for current repository is corrupted, it will "
472 "be redownloaded." ) );
478 if( !packages_cache_exists )
487 for(
size_t i = 0; i < current_repo.
package_list.size(); i++ )
493 repo_cache.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
495 std::ofstream repo_cache_stream( repo_cache.GetFullPath().fn_str() );
496 repo_cache_stream << std::setw( 4 ) << nlohmann::json( current_repo ) << std::endl;
498 std::ofstream packages_cache_stream( packages_cache.GetFullPath().fn_str() );
500 js[
"packages"] = nlohmann::json( current_repo.
package_list );
501 packages_cache_stream << std::setw( 4 ) << js << std::endl;
512 wxFileName resource_file( repo_cache.GetPath(), wxT(
"resources.zip" ) );
516 if( resource_file.FileExists() )
517 mtime = wxFileModificationTime( resource_file.GetFullPath() );
521 std::ofstream resources_stream( resource_file.GetFullPath().fn_str(),
522 std::ios_base::binary );
524 reporter->SetTitle(
_(
"Downloading resources" ) );
530 resources_stream.close();
534 std::ifstream read_stream( resource_file.GetFullPath().fn_str(),
535 std::ios_base::binary );
544 wxLogError(
_(
"Resources file hash doesn't match and will not be used. "
545 "Repository may be corrupted." ) );
548 wxRemoveFile( resource_file.GetFullPath() );
554 wxRemoveFile( resource_file.GetFullPath() );
575 wxLogTrace(
tracePcm, wxS(
"Invalid/Missing repository " ) + aRepositoryId );
579 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair :
m_installed )
591 std::optional<PACKAGE_VERSION> current_version;
593 auto current_version_it =
597 return version.version == entry.current_version;
601 current_version = *current_version_it;
611 return version.version == entry.current_version;
622 return a.parsed_version > b.parsed_version;
634 int epoch = 0, major = 0, minor = 0, patch = 0;
639 wxStringTokenizer version_tokenizer( ver.
version, wxT(
"." ) );
641 major = wxAtoi( version_tokenizer.GetNextToken() );
643 if( version_tokenizer.HasMoreTokens() )
644 minor = wxAtoi( version_tokenizer.GetNextToken() );
646 if( version_tokenizer.HasMoreTokens() )
647 patch = wxAtoi( version_tokenizer.GetNextToken() );
649 ver.
parsed_version = std::make_tuple( epoch, major, minor, patch );
654 auto parse_version_tuple =
655 [](
const wxString& version,
int deflt )
657 int ver_major = deflt;
658 int ver_minor = deflt;
659 int ver_patch = deflt;
661 wxStringTokenizer tokenizer( version, wxT(
"." ) );
663 ver_major = wxAtoi( tokenizer.GetNextToken() );
665 if( tokenizer.HasMoreTokens() )
666 ver_minor = wxAtoi( tokenizer.GetNextToken() );
668 if( tokenizer.HasMoreTokens() )
669 ver_patch = wxAtoi( tokenizer.GetNextToken() );
671 return std::tuple<int, int, int>( ver_major, ver_minor, ver_patch );
682 wxString platform = wxT(
"windows" );
683#elif defined( __APPLE__ )
684 wxString platform = wxT(
"macos" );
686 wxString platform = wxT(
"linux" );
701 return a.parsed_version > b.parsed_version;
706const std::vector<PCM_PACKAGE>&
709 static std::vector<PCM_PACKAGE>
empty{};
727 auto it = std::find_if( aRepositories.begin(), aRepositories.end(),
728 [&](
const auto& new_entry )
730 return new_entry.first == std::get<1>( entry );
733 if( it == aRepositories.end() )
742 for(
const std::pair<wxString, wxString>& repo : aRepositories )
744 std::string url_sha = picosha2::hash256_hex_string( repo.second );
745 m_repository_list.push_back( std::make_tuple( url_sha.substr( 0, 16 ), repo.first,
757 repo_cache.AppendDir( wxT(
"pcm" ) );
758 repo_cache.AppendDir( aRepositoryId );
760 if( repo_cache.DirExists() )
761 repo_cache.Rmdir( wxPATH_RMDIR_RECURSIVE );
766 const wxString& aRepositoryId )
784 if( !aRepositoryId.IsEmpty() )
808 const wxString& aPackageId )
845 return ver.compatible;
856 wxT(
"GetPackageUpdateVersion called on a not installed package" ) );
860 auto installed_ver_it = std::find_if(
864 return ver.version == entry.current_version;
868 wxT(
"Installed package version not found" ) );
870 auto ver_it = std::find_if( aPackage.
versions.begin(), aPackage.
versions.end(),
873 return ver.compatible
874 && installed_ver_it->status >= ver.status
875 && installed_ver_it->parsed_version < ver.parsed_version;
878 return ver_it == aPackage.
versions.end() ? wxString( wxT(
"" ) ) : ver_it->version;
883 return std::chrono::duration_cast<std::chrono::seconds>(
884 std::chrono::system_clock::now().time_since_epoch() ).count();
893 js[
"packages"] = nlohmann::json::array();
895 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair :
m_installed )
897 js[
"packages"].emplace_back( pair.second );
901 std::ofstream stream( f.GetFullPath().fn_str() );
903 stream << std::setw( 4 ) << js << std::endl;
905 catch( nlohmann::detail::exception& )
914 std::vector<PCM_INSTALLATION_ENTRY> v;
917 [&v](
const std::pair<const wxString, PCM_INSTALLATION_ENTRY>& entry )
919 v.push_back( entry.second );
922 std::sort( v.begin(), v.end(),
925 return ( a.install_timestamp < b.install_timestamp )
926 || ( a.install_timestamp == b.install_timestamp
927 && a.package.identifier < b.package.identifier );
938 wxT(
"Installed package not found." ) );
940 return m_installed.at( aPackageId ).current_version;
963 const wxString& aSearchTerm )
965 wxArrayString terms = wxStringTokenize( aSearchTerm.Lower(), wxS(
" " ), wxTOKEN_STRTOK );
968 const auto find_term_matches =
969 [&](
const wxString& str )
972 wxString lower = str.Lower();
974 for(
const wxString& term : terms )
976 if( lower.Find( term ) != wxNOT_FOUND )
984 if( terms.size() == 1 && terms[0] == aPackage.
identifier )
987 if( terms.size() == 1 && find_term_matches( aPackage.
identifier ) )
991 rank += 500 * find_term_matches( aPackage.
name );
994 for(
const std::string& tag : aPackage.
tags )
995 rank += 100 * find_term_matches( wxString( tag ) );
998 rank += 10 * find_term_matches( aPackage.
description );
1002 rank += find_term_matches( aPackage.
author.
name );
1005 rank += 3 * find_term_matches( aPackage.
maintainer->name );
1008 for(
const std::pair<const std::string, wxString>& entry : aPackage.
resources )
1010 rank += find_term_matches( entry.first );
1011 rank += find_term_matches( entry.second );
1015 if( terms.size() == 1 && terms[0] == aPackage.
license )
1022std::unordered_map<wxString, wxBitmap>
1025 std::unordered_map<wxString, wxBitmap> bitmaps;
1028 resources_file.AppendDir( wxT(
"pcm" ) );
1029 resources_file.AppendDir( aRepositoryId );
1031 if( !resources_file.FileExists() )
1034 wxFFileInputStream stream( resources_file.GetFullPath() );
1035 wxZipInputStream
zip( stream );
1037 if( !
zip.IsOk() ||
zip.GetTotalEntries() == 0 )
1040 for( wxArchiveEntry* entry =
zip.GetNextEntry(); entry; entry =
zip.GetNextEntry() )
1042 wxArrayString path_parts = wxSplit( entry->GetName(), wxFileName::GetPathSeparator(),
1045 if( path_parts.size() != 2 || path_parts[1] != wxT(
"icon.png" ) )
1050 wxMemoryInputStream image_stream(
zip, entry->GetSize() );
1051 wxImage
image( image_stream, wxBITMAP_TYPE_PNG );
1052 bitmaps.emplace( path_parts[0], wxBitmap(
image ) );
1057 wxLogTrace( wxT(
"Error loading png bitmap for entry %s from %s" ), entry->GetName(),
1058 resources_file.GetFullPath() );
1068 std::unordered_map<wxString, wxBitmap> bitmaps;
1071 resources_dir_fn.AppendDir( wxT(
"resources" ) );
1072 wxDir resources_dir( resources_dir_fn.GetPath() );
1074 if( !resources_dir.IsOpened() )
1078 bool more = resources_dir.GetFirst( &subdir, wxEmptyString, wxDIR_DIRS | wxDIR_HIDDEN );
1082 wxFileName icon( resources_dir_fn.GetPath(), wxT(
"icon.png" ) );
1083 icon.AppendDir( subdir );
1085 if( icon.FileExists() )
1087 wxString actual_package_id = subdir;
1088 actual_package_id.Replace(
'_',
'.' );
1092 wxBitmap bitmap( icon.GetFullPath(), wxBITMAP_TYPE_PNG );
1093 bitmaps.emplace( actual_package_id, bitmap );
1098 wxLogTrace( wxT(
"Error loading png bitmap from %s" ), icon.GetFullPath() );
1102 more = resources_dir.GetNext( &subdir );
1128 std::unordered_set<wxString> repo_ids;
1130 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair :
m_installed )
1132 if( !pair.second.pinned )
1133 repo_ids.insert( pair.second.repository_id );
1139 if( repo_ids.count( repository_id ) == 0 )
1143 _(
"Fetching repository..." ) );
1154 int availableUpdateCount = 0;
1157 for( std::pair<const wxString, PCM_INSTALLATION_ENTRY>& pair :
m_installed )
1169 availableUpdateCount++;
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.
std::shared_ptr< BACKGROUND_JOB > Create(const wxString &aName)
Creates a background job with the given name.
void Remove(std::shared_ptr< BACKGROUND_JOB > job)
Removes the given background job from any lists and frees it.
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.
Instantiate the current locale within a scope in which you are expecting exceptions to be thrown.
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.
static wxString GetUserSettingsPath()
Return the user configuration path used to store KiCad's configuration files.
virtual ENV_VAR_MAP & GetLocalEnvVariables() const
virtual BACKGROUND_JOBS_MONITOR & GetBackgroundJobMonitor() const
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.
int GetPackageSearchRank(const PCM_PACKAGE &aPackage, const wxString &aSearchTerm)
Get the approximate measure of how much given package matches the search term.
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.
static void PreparePackage(PCM_PACKAGE &aPackage)
Parses version strings and calculates compatibility.
PLUGIN_CONTENT_MANAGER(std::function< void(int)> aAvailableUpdateCallbac)
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.
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
std::shared_ptr< BACKGROUND_JOB > m_updateBackgroundJob
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.
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).
void error(const json::json_pointer &ptr, const json &instance, const std::string &message) override
static bool empty(const wxTextEntryBase *aCtrl)
Functions related to environment variables, including help functions.
static const wxChar tracePcm[]
Flag to enable PCM debugging output.
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/.
KICOMMON_API std::optional< wxString > GetVersionedEnvVarValue(const std::map< wxString, ENV_VAR_ITEM > &aMap, const wxString &aBaseName)
Attempts to retrieve the value of a versioned environment variable, such as KICAD8_TEMPLATE_DIR.
std::vector< std::pair< wxString, wxString > > STRING_PAIR_LIST
const std::unordered_set< wxString > PCM_PACKAGE_DIRECTORIES({ "plugins", "footprints", "3dmodels", "symbols", "resources", "colors", "templates", "scripts" })
< Contains list of all valid directories that get extracted from a package archive
PGM_BASE & Pgm()
The global Program "get" accessor.
std::vector< FAB_LAYER_COLOR > dummy
< 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().