KiCad PCB EDA Suite
Loading...
Searching...
No Matches
pcm_task_manager.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-2021 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// kicad_curl_easy.h **must be** included before any wxWidgets header to avoid conflicts
21// at least on Windows/msys2
24
25#include <paths.h>
26#include "pcm_task_manager.h"
27#include <reporter.h>
28#include <wxstream_helper.h>
29
30#include <fstream>
31#include <thread>
32#include <unordered_set>
33#include <wx/dir.h>
34#include <wx/filename.h>
35#include <wx/msgdlg.h>
36#include <wx/sstream.h>
37#include <wx/wfstream.h>
38#include <wx/zipstrm.h>
39
40
42 std::forward_list<wxRegEx>& aKeepOnUpdate )
43{
44 auto compile_regex = [&]( const wxString& regex )
45 {
46 aKeepOnUpdate.emplace_front( regex, wxRE_DEFAULT );
47
48 if( !aKeepOnUpdate.front().IsValid() )
49 aKeepOnUpdate.pop_front();
50 };
51
52 std::for_each( pkg.keep_on_update.begin(), pkg.keep_on_update.end(), compile_regex );
53 std::for_each( ver.keep_on_update.begin(), ver.keep_on_update.end(), compile_regex );
54}
55
56
58 const wxString& aRepositoryId, const bool isUpdate )
59{
60 PCM_TASK download_task = [aPackage, aVersion, aRepositoryId, isUpdate, this]() -> PCM_TASK_MANAGER::STATUS
61 {
62 wxFileName file_path( PATHS::GetUserCachePath(), "" );
63 file_path.AppendDir( "pcm" );
64 file_path.SetFullName( wxString::Format( "%s_v%s.zip", aPackage.identifier, aVersion ) );
65
66 auto find_pkgver = std::find_if( aPackage.versions.begin(), aPackage.versions.end(),
67 [&aVersion]( const PACKAGE_VERSION& pv )
68 {
69 return pv.version == aVersion;
70 } );
71
72 if( find_pkgver == aPackage.versions.end() )
73 {
74 m_reporter->PCMReport( wxString::Format( _( "Version %s of package %s not found!" ),
75 aVersion, aPackage.identifier ),
78 }
79
80 if( !wxDirExists( file_path.GetPath() )
81 && !wxFileName::Mkdir( file_path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
82 {
83 m_reporter->PCMReport( _( "Unable to create download directory!" ),
86 }
87
88 int code = downloadFile( file_path.GetFullPath(), *find_pkgver->download_url );
89
90 if( code != CURLE_OK )
91 {
92 // Cleanup after ourselves and exit
93 wxRemoveFile( file_path.GetFullPath() );
95 }
96
97 PCM_TASK install_task = [aPackage, aVersion, aRepositoryId, file_path, isUpdate, this]()
98 {
99 return installDownloadedPackage( aPackage, aVersion, aRepositoryId, file_path, isUpdate );
100 };
101
102 m_install_queue.push( install_task );
103
105 };
106
107 m_download_queue.push( download_task );
109}
110
111
112int PCM_TASK_MANAGER::downloadFile( const wxString& aFilePath, const wxString& url )
113{
114 TRANSFER_CALLBACK callback = [&]( size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow )
115 {
116 if( dltotal > 1024 )
117 m_reporter->SetDownloadProgress( dlnow, dltotal );
118 else
119 m_reporter->SetDownloadProgress( 0.0, 0.0 );
120
121 return m_reporter->IsCancelled();
122 };
123
124 std::ofstream out( aFilePath.ToUTF8(), std::ofstream::binary );
125
126 KICAD_CURL_EASY curl;
127 curl.SetOutputStream( &out );
128 curl.SetURL( url.ToUTF8().data() );
129 curl.SetFollowRedirects( true );
130 curl.SetTransferCallback( callback, 250000L );
131
132 m_reporter->PCMReport( wxString::Format( _( "Downloading package url: '%s'" ), url ),
134
135 int code = curl.Perform();
136
137 out.close();
138
139 uint64_t download_total;
140
141 if( CURLE_OK == curl.GetTransferTotal( download_total ) )
142 m_reporter->SetDownloadProgress( download_total, download_total );
143
144 if( code != CURLE_OK && code != CURLE_ABORTED_BY_CALLBACK )
145 {
146 m_reporter->PCMReport( wxString::Format( _( "Failed to download url %s\n%s" ), url,
147 curl.GetErrorText( code ) ),
149 }
150
151 return code;
152}
153
154
156 const wxString& aVersion,
157 const wxString& aRepositoryId,
158 const wxFileName& aFilePath, const bool isUpdate )
159{
160 auto pkgver = std::find_if( aPackage.versions.begin(), aPackage.versions.end(),
161 [&aVersion]( const PACKAGE_VERSION& pv )
162 {
163 return pv.version == aVersion;
164 } );
165
166 if( pkgver == aPackage.versions.end() )
167 {
168 m_reporter->PCMReport( wxString::Format( _( "Version %s of package %s not found!" ),
169 aVersion, aPackage.identifier ),
172 }
173
174 // wxRegEx is not CopyConstructible hence the weird choice of forward_list
175 std::forward_list<wxRegEx> keep_on_update;
176
177 if( isUpdate )
178 compile_keep_on_update_regex( aPackage, *pkgver, keep_on_update );
179
180 const std::optional<wxString>& hash = pkgver->download_sha256;
181 bool hash_match = true;
182
183 if( hash )
184 {
185 std::ifstream stream( aFilePath.GetFullPath().fn_str(), std::ios::binary );
186 hash_match = m_pcm->VerifyHash( stream, *hash );
187 }
188
189 if( !hash_match )
190 {
191 m_reporter->PCMReport( wxString::Format( _( "Downloaded archive hash for package "
192 "%s does not match repository entry. "
193 "This may indicate a problem with the "
194 "package, if the issue persists "
195 "report this to repository maintainers." ),
196 aPackage.name ),
198 wxRemoveFile( aFilePath.GetFullPath() );
200 }
201 else
202 {
203 if( isUpdate )
204 {
205 m_reporter->PCMReport(
206 wxString::Format( _( "Removing previous version of package '%s'." ),
207 aPackage.name ),
209
210 deletePackageDirectories( aPackage.identifier, keep_on_update );
211 }
212
213 m_reporter->PCMReport(
214 wxString::Format( _( "Installing package '%s'." ), aPackage.name ),
216
217 if( extract( aFilePath.GetFullPath(), aPackage.identifier, true ) )
218 {
219 m_pcm->MarkInstalled( aPackage, pkgver->version, aRepositoryId );
220 }
221 else
222 {
223 // Cleanup possibly partially extracted package
225 }
226
227 std::unique_lock lock( m_changed_package_types_guard );
228 m_changed_package_types.insert( aPackage.type );
229 }
230
231 wxRemoveFile( aFilePath.GetFullPath() );
233}
234
235
236bool PCM_TASK_MANAGER::extract( const wxString& aFilePath, const wxString& aPackageId,
237 bool isMultiThreaded )
238{
239 wxFFileInputStream stream( aFilePath );
240 wxZipInputStream zip( stream );
241
242 wxLogNull no_wx_logging;
243
244 int entries = zip.GetTotalEntries();
245 int extracted = 0;
246
247 wxArchiveEntry* entry = zip.GetNextEntry();
248
249 if( !zip.IsOk() )
250 {
251 m_reporter->PCMReport( _( "Error extracting file!" ), RPT_SEVERITY_ERROR );
252 return false;
253 }
254
255 // Namespace delimiter changed on disk to allow flat loading of Python modules
256 wxString clean_package_id = aPackageId;
257 clean_package_id.Replace( '.', '_' );
258
259 for( ; entry; entry = zip.GetNextEntry() )
260 {
261 wxArrayString path_parts =
262 wxSplit( entry->GetName(), wxFileName::GetPathSeparator(), (wxChar) 0 );
263
264 if( path_parts.size() < 2
265 || PCM_PACKAGE_DIRECTORIES.find( path_parts[0] ) == PCM_PACKAGE_DIRECTORIES.end()
266 || path_parts[path_parts.size() - 1].IsEmpty() )
267 {
268 // Ignore files in the root of the archive and files outside of package dirs.
269 continue;
270 }
271
272
273 // Transform paths from
274 // <PackageRoot>/$folder/$contents
275 // To
276 // $KICAD7_3RD_PARTY/$folder/$package_id/$contents
277 path_parts.Insert( clean_package_id, 1 );
278 path_parts.Insert( m_pcm->Get3rdPartyPath(), 0 );
279
280 wxString fullname = wxJoin( path_parts, wxFileName::GetPathSeparator(), (wxChar) 0 );
281
282 // Ensure the target directory exists and create it if not.
283 wxString t_path = wxPathOnly( fullname );
284
285 if( !wxDirExists( t_path ) )
286 {
287 wxFileName::Mkdir( t_path, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
288 }
289
290 wxTempFileOutputStream out( fullname );
291
292 if( !( CopyStreamData( zip, out, entry->GetSize() ) && out.Commit() ) )
293 {
294 m_reporter->PCMReport( _( "Error extracting file!" ), RPT_SEVERITY_ERROR );
295 return false;
296 }
297
298 extracted++;
299 m_reporter->SetPackageProgress( extracted, entries );
300
301 if( !isMultiThreaded )
302 m_reporter->KeepRefreshing( false );
303
304 if( m_reporter->IsCancelled() )
305 break;
306 }
307
308 zip.CloseEntry();
309
310 if( m_reporter->IsCancelled() )
311 {
312 m_reporter->PCMReport( _( "Aborting package installation." ), RPT_SEVERITY_INFO );
313 return false;
314 }
315
316 m_reporter->SetPackageProgress( entries, entries );
317
318 return true;
319}
320
321
323 const wxString& aFilePath )
324{
325 wxFFileInputStream stream( aFilePath );
326
327 if( !stream.IsOk() )
328 {
329 wxLogError( _( "Could not open archive file." ) );
331 }
332
333 wxZipInputStream zip( stream );
334
335 if( !zip.IsOk() )
336 {
337 wxLogError( _( "Invalid archive file format." ) );
339 }
340
341 nlohmann::json metadata;
342
343 for( wxArchiveEntry* entry = zip.GetNextEntry(); entry != nullptr; entry = zip.GetNextEntry() )
344 {
345 // Find and load metadata.json
346 if( entry->GetName() != "metadata.json" )
347 continue;
348
349 wxStringOutputStream strStream;
350
351 if( CopyStreamData( zip, strStream, entry->GetSize() ) )
352 {
353 try
354 {
355 metadata = nlohmann::json::parse( strStream.GetString().ToUTF8().data() );
356 m_pcm->ValidateJson( metadata );
357 }
358 catch( const std::exception& e )
359 {
360 wxLogError( wxString::Format( _( "Unable to parse package metadata:\n\n%s" ),
361 e.what() ) );
362 break;
363 }
364 }
365 }
366
367 if( metadata.empty() )
368 {
369 wxLogError( _( "Archive does not contain a valid metadata.json file" ) );
371 }
372
373 PCM_PACKAGE package = metadata.get<PCM_PACKAGE>();
375
376 if( package.versions.size() != 1 )
377 {
378 wxLogError( _( "Archive metadata must have a single version defined" ) );
380 }
381
382 if( !package.versions[0].compatible
383 && wxMessageBox( _( "This package version is incompatible with your KiCad version or "
384 "platform. Are you sure you want to install it anyway?" ),
385 _( "Install package" ), wxICON_EXCLAMATION | wxYES_NO, aParent )
386 == wxNO )
387 {
389 }
390
391 bool isUpdate = false;
392 // wxRegEx is not CopyConstructible hence the weird choice of forward_list
393 std::forward_list<wxRegEx> keep_on_update;
394 const std::vector<PCM_INSTALLATION_ENTRY> installed_packages = m_pcm->GetInstalledPackages();
395
396 if( std::find_if( installed_packages.begin(), installed_packages.end(),
397 [&]( const PCM_INSTALLATION_ENTRY& entry )
398 {
399 return entry.package.identifier == package.identifier;
400 } )
401 != installed_packages.end() )
402 {
403 if( wxMessageBox(
404 wxString::Format(
405 _( "Package with identifier %s is already installed. "
406 "Would you like to update it to the version from selected file?" ),
407 package.identifier ),
408 _( "Update package" ), wxICON_EXCLAMATION | wxYES_NO, aParent )
409 == wxNO )
411
412 isUpdate = true;
413
414 compile_keep_on_update_regex( package, package.versions[0], keep_on_update );
415 }
416
417 m_reporter = std::make_unique<DIALOG_PCM_PROGRESS>( aParent, false );
418#ifdef __WXMAC__
419 m_reporter->ShowWindowModal();
420#else
421 m_reporter->Show();
422#endif
423
424 if( isUpdate )
425 {
426 m_reporter->PCMReport( wxString::Format( _( "Removing previous version of package '%s'." ),
427 package.name ),
429
430 deletePackageDirectories( package.identifier, keep_on_update );
431 }
432
433 if( extract( aFilePath, package.identifier, false ) )
434 m_pcm->MarkInstalled( package, package.versions[0].version, "" );
435
436 m_reporter->SetFinished();
437 m_reporter->KeepRefreshing( false );
438 m_reporter->Destroy();
439 m_reporter.reset();
440
441 aParent->Raise();
442
443 std::unique_lock lock( m_changed_package_types_guard );
444 m_changed_package_types.insert( package.type );
446}
447
448
449class PATH_COLLECTOR : public wxDirTraverser
450{
451private:
452 std::vector<wxString>& m_files;
453 std::vector<wxString>& m_dirs;
454
455public:
456 explicit PATH_COLLECTOR( std::vector<wxString>& aFiles, std::vector<wxString>& aDirs ) :
457 m_files( aFiles ), m_dirs( aDirs )
458 {
459 }
460
461 wxDirTraverseResult OnFile( const wxString& aFilePath ) override
462 {
463 m_files.push_back( aFilePath );
464 return wxDIR_CONTINUE;
465 }
466
467 wxDirTraverseResult OnDir( const wxString& dirPath ) override
468 {
469 m_dirs.push_back( dirPath );
470 return wxDIR_CONTINUE;
471 }
472};
473
474
475void PCM_TASK_MANAGER::deletePackageDirectories( const wxString& aPackageId,
476 const std::forward_list<wxRegEx>& aKeep )
477{
478 // Namespace delimiter changed on disk to allow flat loading of Python modules
479 wxString clean_package_id = aPackageId;
480 clean_package_id.Replace( '.', '_' );
481
482 int path_prefix_len = m_pcm->Get3rdPartyPath().Length();
483
484 auto sort_func = []( const wxString& a, const wxString& b )
485 {
486 if( a.length() > b.length() )
487 return true;
488 if( a.length() < b.length() )
489 return false;
490
491 if( a != b )
492 return a < b;
493
494 return false;
495 };
496
497 for( const wxString& dir : PCM_PACKAGE_DIRECTORIES )
498 {
499 wxFileName d( m_pcm->Get3rdPartyPath(), "" );
500 d.AppendDir( dir );
501 d.AppendDir( clean_package_id );
502
503 if( !d.DirExists() )
504 continue;
505
506 m_reporter->PCMReport( wxString::Format( _( "Removing directory %s" ), d.GetPath() ),
508
509 if( aKeep.empty() )
510 {
511 if( !d.Rmdir( wxPATH_RMDIR_RECURSIVE ) )
512 {
513 m_reporter->PCMReport(
514 wxString::Format( _( "Failed to remove directory %s" ), d.GetPath() ),
516 }
517 }
518 else
519 {
520 std::vector<wxString> files;
521 std::vector<wxString> dirs;
522 PATH_COLLECTOR collector( files, dirs );
523
524 wxDir( d.GetFullPath() )
525 .Traverse( collector, wxEmptyString, wxDIR_DEFAULT | wxDIR_NO_FOLLOW );
526
527 // Do a poor mans post order traversal by sorting paths in reverse length order
528 std::sort( files.begin(), files.end(), sort_func );
529 std::sort( dirs.begin(), dirs.end(), sort_func );
530
531 // Delete files that don't match any of the aKeep regexs
532 for( const wxString& file : files )
533 {
534 bool del = true;
535
536 for( const wxRegEx& re : aKeep )
537 {
538 wxString tmp = file.Mid( path_prefix_len );
539 tmp.Replace( "\\", "/" );
540
541 if( re.Matches( tmp ) )
542 {
543 del = false;
544 break;
545 }
546 }
547
548 if( del )
549 wxRemoveFile( file );
550 }
551
552 // Delete any empty dirs
553 for( const wxString& empty_dir : dirs )
554 {
555 wxFileName dname( empty_dir, "" );
556 dname.Rmdir(); // not passing any flags here will only remove empty directories
557 }
558 }
559 }
560}
561
562
564{
565 PCM_TASK task = [aPackage, this]
566 {
568
569 m_pcm->MarkUninstalled( aPackage );
570
571 std::unique_lock lock( m_changed_package_types_guard );
572 m_changed_package_types.insert( aPackage.type );
573
574 m_reporter->PCMReport(
575 wxString::Format( _( "Package %s uninstalled" ), aPackage.name ),
578 };
579
580 m_install_queue.push( task );
582}
583
584
585void PCM_TASK_MANAGER::RunQueue( wxWindow* aParent )
586{
587 m_reporter = std::make_unique<DIALOG_PCM_PROGRESS>( aParent );
588
589 m_reporter->SetNumPhases( m_download_queue.size() + m_install_queue.size() );
590#ifdef __WXMAC__
591 m_reporter->ShowWindowModal();
592#else
593 m_reporter->Show();
594#endif
595
596 wxSafeYield();
597
598 std::mutex mutex;
599 std::condition_variable condvar;
600 bool download_complete = false;
601 int count_tasks = 0;
602 int count_failed_tasks = 0;
603 int count_success_tasks = 0;
604
605 std::thread download_thread(
606 [&]()
607 {
608 while( !m_download_queue.empty() && !m_reporter->IsCancelled() )
609 {
610 PCM_TASK task;
611 m_download_queue.pop( task );
612 task();
613 condvar.notify_all();
614 }
615
616 std::unique_lock<std::mutex> lock( mutex );
617 download_complete = true;
618 condvar.notify_all();
619 } );
620
621 std::thread install_thread(
622 [&]()
623 {
624 std::unique_lock<std::mutex> lock( mutex );
625
626 do
627 {
628 condvar.wait( lock,
629 [&]()
630 {
631 return download_complete || !m_install_queue.empty()
632 || m_reporter->IsCancelled();
633 } );
634
635 lock.unlock();
636
637 while( !m_install_queue.empty() && !m_reporter->IsCancelled() )
638 {
639 PCM_TASK task;
640 m_install_queue.pop( task );
641 PCM_TASK_MANAGER::STATUS task_status = task();
642
643 count_tasks++;
644
645 if( task_status == PCM_TASK_MANAGER::STATUS::SUCCESS )
646 count_success_tasks++;
647 else if( task_status != PCM_TASK_MANAGER::STATUS::INITIALIZED )
648 count_failed_tasks++;
649
650 m_reporter->AdvancePhase();
651 }
652
653 lock.lock();
654
655 } while( ( !m_install_queue.empty() || !download_complete )
656 && !m_reporter->IsCancelled() );
657
658 if( count_failed_tasks != 0 )
659 {
660 m_reporter->PCMReport(
661 wxString::Format( _( "%d out of %d operations failed." ), count_failed_tasks, count_tasks ),
663 }
664 else
665 {
666 if( count_success_tasks == count_tasks )
667 {
668 m_reporter->PCMReport( _( "All operations completed successfully." ), RPT_SEVERITY_INFO );
669 }
670 else
671 {
672 m_reporter->PCMReport(
673 wxString::Format( _( "%d out of %d operations were initialized but not successful." ),
674 count_tasks - count_success_tasks, count_tasks ),
676 }
677 }
678
679 m_reporter->SetFinished();
680 } );
681
682 m_reporter->KeepRefreshing( true );
683
684 download_thread.join();
685 install_thread.join();
686
687 // Destroy the reporter only after the threads joined
688 // Incase the reporter terminated due to cancellation
689 m_reporter->Destroy();
690 m_reporter.reset();
691
692 aParent->Raise();
693}
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.
int GetTransferTotal(uint64_t &aDownloadedBytes) const
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 GetUserCachePath()
Gets the stock (install) 3d viewer plugins path.
Definition: paths.cpp:344
PATH_COLLECTOR(std::vector< wxString > &aFiles, std::vector< wxString > &aDirs)
wxDirTraverseResult OnDir(const wxString &dirPath) override
std::vector< wxString > & m_dirs
std::vector< wxString > & m_files
wxDirTraverseResult OnFile(const wxString &aFilePath) override
void deletePackageDirectories(const wxString &aPackageId, const std::forward_list< wxRegEx > &aKeep={})
Delete all package files.
SYNC_QUEUE< PCM_TASK > m_install_queue
std::shared_ptr< PLUGIN_CONTENT_MANAGER > m_pcm
SYNC_QUEUE< PCM_TASK > m_download_queue
std::function< STATUS()> PCM_TASK
int downloadFile(const wxString &aFilePath, const wxString &aUrl)
Download URL to a file.
PCM_TASK_MANAGER::STATUS DownloadAndInstall(const PCM_PACKAGE &aPackage, const wxString &aVersion, const wxString &aRepositoryId, const bool isUpdate)
Enqueue package download and installation.
bool extract(const wxString &aFilePath, const wxString &aPackageId, bool isMultiThreaded)
Extract package archive.
void RunQueue(wxWindow *aParent)
Run queue of pending actions.
std::mutex m_changed_package_types_guard
PCM_TASK_MANAGER::STATUS installDownloadedPackage(const PCM_PACKAGE &aPackage, const wxString &aVersion, const wxString &aRepositoryId, const wxFileName &aFilePath, const bool isUpdate)
Installs downloaded package archive.
std::unique_ptr< DIALOG_PCM_PROGRESS > m_reporter
PCM_TASK_MANAGER::STATUS Uninstall(const PCM_PACKAGE &aPackage)
Enqueue package uninstallation.
PCM_TASK_MANAGER::STATUS InstallFromFile(wxWindow *aParent, const wxString &aFilePath)
Installs package from an archive file on disk.
std::unordered_set< PCM_PACKAGE_TYPE > m_changed_package_types
static void PreparePackage(PCM_PACKAGE &aPackage)
Parses version strings and calculates compatibility.
Definition: pcm.cpp:615
bool pop(T &aReceiver)
Pop a value if the queue into the provided variable.
Definition: sync_queue.h:63
bool empty() const
Return true if the queue is empty.
Definition: sync_queue.h:82
size_t size() const
Return the size of the queue.
Definition: sync_queue.h:91
void push(T const &aValue)
Push a value onto the queue.
Definition: sync_queue.h:41
#define _(s)
std::function< int(size_t, size_t, size_t, size_t)> TRANSFER_CALLBACK
Wrapper interface around the curl_easy API/.
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
void compile_keep_on_update_regex(const PCM_PACKAGE &pkg, const PACKAGE_VERSION &ver, std::forward_list< wxRegEx > &aKeepOnUpdate)
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_INFO
< Package version metadata Package metadata
Definition: pcm_data.h:75
std::vector< std::string > keep_on_update
Definition: pcm_data.h:86
Definition: pcm_data.h:139
Repository reference to a resource.
Definition: pcm_data.h:96
wxString identifier
Definition: pcm_data.h:100
wxString name
Definition: pcm_data.h:97
std::vector< PACKAGE_VERSION > versions
Definition: pcm_data.h:108
PCM_PACKAGE_TYPE type
Definition: pcm_data.h:101
std::vector< std::string > keep_on_update
Definition: pcm_data.h:107
static bool CopyStreamData(wxInputStream &inputStream, wxOutputStream &outputStream, wxFileOffset size)