OpenCPN Partial API docs
download_mgr.cpp
1 /******************************************************************************
2  *
3  * Project: OpenCPN
4  *
5  ***************************************************************************
6  * Copyright (C) 2019 Alec Leamas *
7  * *
8  * This program is free software; you can redistribute it and/or modify *
9  * it under the terms of the GNU General Public License as published by *
10  * the Free Software Foundation; either version 2 of the License, or *
11  * (at your option) any later version. *
12  * *
13  * This program is distributed in the hope that it will be useful, *
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16  * GNU General Public License for more details. *
17  * *
18  * You should have received a copy of the GNU General Public License *
19  * along with this program; if not, write to the *
20  * Free Software Foundation, Inc., *
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
22  ***************************************************************************
23  */
24 
25 #include "config.h"
26 
27 #include <fstream>
28 #include <set>
29 #include <sstream>
30 
31 #include <wx/bitmap.h>
32 #include <wx/button.h>
33 #include <wx/debug.h>
34 #include <wx/file.h>
35 #include <wx/image.h>
36 #include <wx/log.h>
37 #include <wx/panel.h>
38 #include <wx/progdlg.h>
39 #include <wx/sizer.h>
40 #include <wx/statline.h>
41 #include <wx/uri.h>
42 
43 #include "catalog_mgr.h"
44 #include "download_mgr.h"
45 #include "model/downloader.h"
46 #include "OCPNPlatform.h"
47 #include "picosha2.h"
48 #include "model/plugin_handler.h"
49 #include "model/plugin_cache.h"
50 #include "pluginmanager.h"
51 #include "model/semantic_vers.h"
52 #include "styles.h"
53 #include "svg_utils.h"
54 
55 extern PlugInManager* g_pi_manager;
56 extern ocpnStyle::StyleManager* g_StyleManager;
57 extern OCPNPlatform* g_Platform;
58 
59 #undef major // walk around gnu's major() and minor() macros.
60 #undef minor
61 
62 // Main window reload event
63 wxDEFINE_EVENT(EVT_PLUGINS_RELOAD, wxCommandEvent);
64 
65 namespace download_mgr {
66 
70 static bool checksum_ok(const std::string& path,
71  const PluginMetadata& metadata) {
72  wxLogDebug("Checksum test on %s", metadata.name.c_str());
73  if (metadata.checksum == "") {
74  wxLogDebug("No metadata checksum, aborting check,");
75  return true;
76  }
77  const size_t pos = metadata.checksum.find(':');
78  std::string checksum(metadata.checksum);
79  if (pos == std::string::npos) {
80  checksum = std::string("sha256:") + checksum;
81  }
82  std::ifstream f(path, std::ios::binary);
83  picosha2::hash256_one_by_one hasher;
84  while (!f.eof()) {
85  char buff[2048];
86  f.read(buff, sizeof(buff));
87  const std::string block(buff, f.gcount());
88  hasher.process(block.begin(), block.end());
89  }
90  hasher.finish();
91  std::string tarball_hash =
92  std::string("sha256:") + picosha2::get_hash_hex_string(hasher);
93 
94  if (tarball_hash == checksum) {
95  wxLogDebug("Checksum ok: %s", tarball_hash.c_str());
96  return true;
97  }
98  wxLogMessage("Checksum fail on %s, tarball: %s, metadata: %s",
99  metadata.name.c_str(), tarball_hash.c_str(), checksum.c_str());
100  return false;
101 }
102 
107 static ssize_t PlugInIxByName(const std::string name,
108  const ArrayOfPlugIns* plugins) {
109  for (unsigned i = 0; i < plugins->GetCount(); i += 1) {
110  if (name == plugins->Item(i)->m_common_name.Lower().ToStdString()) {
111  return i;
112  }
113  }
114  return -1;
115 }
116 
118 static PlugInContainer* PlugInByName(const std::string name,
119  const ArrayOfPlugIns* plugins) {
120  auto ix = PlugInIxByName(name, plugins);
121  return ix == -1 ? 0 : plugins->Item(ix);
122 }
123 
125 static void LoadPNGIcon(const char* path, int size, wxBitmap& bitmap) {
126  wxPNGHandler handler;
127  if (!wxImage::FindHandler(handler.GetName())) {
128  wxImage::AddHandler(new wxPNGHandler());
129  }
130  auto img = new wxImage();
131  bool ok = img->LoadFile(path, wxBITMAP_TYPE_PNG);
132  if (!ok) {
133  bitmap = wxBitmap();
134  return;
135  }
136  img->Rescale(size, size);
137  bitmap = wxBitmap(*img);
138 }
139 
146 class PluginIconPanel : public wxPanel {
147 public:
148  PluginIconPanel(wxWindow* parent, std::string plugin_name)
149  : wxPanel(parent), m_plugin_name(plugin_name) {
150  auto size = GetClientSize();
151  auto minsize = GetTextExtent("OpenCPN");
152  SetMinClientSize(wxSize(minsize.GetWidth(), size.GetHeight()));
153  Layout();
154  Bind(wxEVT_PAINT, &PluginIconPanel::OnPaint, this);
155  }
156 
157  void OnPaint(wxPaintEvent& event) {
158  auto size = GetClientSize();
159  int minsize = wxMin(size.GetHeight(), size.GetWidth());
160  auto offset = minsize / 10;
161 
162  LoadIcon(m_plugin_name.c_str(), m_bitmap, 2 * minsize / 3);
163  wxPaintDC dc(this);
164  if (!m_bitmap.IsOk()) {
165  wxLogMessage("AddPluginPanel: bitmap is not OK!");
166  return;
167  }
168  dc.DrawBitmap(m_bitmap, offset, offset, true);
169  }
170 
171 protected:
172  wxBitmap m_bitmap;
173  const std::string m_plugin_name;
174 
175  void LoadIcon(const char* plugin_name, wxBitmap& bitmap, int size = 32) {
176  wxFileName path(g_Platform->GetSharedDataDir(), plugin_name);
177  path.AppendDir("uidata");
178  path.AppendDir("plugins");
179  path.SetExt("svg");
180  bool ok = false;
181  if (path.IsFileReadable()) {
182  bitmap = LoadSVG(path.GetFullPath(), size, size);
183  ok = bitmap.IsOk();
184  }
185  if (!ok) {
186  path.SetExt("png");
187  if (path.IsFileReadable()) {
188  LoadPNGIcon(path.GetFullPath(), size, bitmap);
189  ok = bitmap.IsOk();
190  }
191  }
192  if (!ok) {
193  auto style = g_StyleManager->GetCurrentStyle();
194  bitmap = wxBitmap(style->GetIcon(_T("default_pi")));
195  }
196  }
197 };
198 
200 class InstallButton : public wxPanel {
201 public:
202  InstallButton(wxWindow* parent, PluginMetadata metadata)
203  : wxPanel(parent), m_metadata(metadata), m_remove(false) {
204  PlugInContainer* found =
205  PlugInByName(metadata.name,
206  PluginLoader::getInstance()->GetPlugInArray());
207  std::string label(_("Install"));
208  if (found) {
209  label = getUpdateLabel(found, metadata);
210  m_remove = true;
211  }
212  auto button = new wxButton(this, wxID_ANY, label);
213  auto pluginHandler = PluginHandler::getInstance();
214  button->Enable(pluginHandler->isPluginWritable(metadata.name));
215  auto box = new wxBoxSizer(wxHORIZONTAL);
216  box->Add(button);
217  SetSizer(box);
218  Bind(wxEVT_COMMAND_BUTTON_CLICKED, &InstallButton::OnClick, this);
219  }
220 
221  void OnClick(wxCommandEvent& event) {
222  auto path = ocpn::lookup_tarball(m_metadata.tarball_url.c_str());
223  if (m_remove && path != "") {
224  wxLogMessage("Uninstalling %s", m_metadata.name.c_str());
225  PluginHandler::getInstance()->uninstall(m_metadata.name);
226  }
227  wxLogMessage("Installing %s", m_metadata.name.c_str());
228 
229  auto pluginHandler = PluginHandler::getInstance();
230  bool cacheResult = pluginHandler->installPluginFromCache(m_metadata);
231 
232  if (!cacheResult) {
233  auto downloader = new GuiDownloader(this, m_metadata);
234  downloader->run(this, m_remove);
235  auto loader = PluginLoader::getInstance();
236  auto pic = PlugInByName(m_metadata.name, loader->GetPlugInArray());
237  if (!pic) {
238  wxLogMessage("Installation of %s failed", m_metadata.name.c_str());
239  return;
240  }
241  auto upwards = GetParent()->GetParent()->GetParent();
242  auto main_window = dynamic_cast<PluginDownloadDialog*>(upwards);
243  wxASSERT(main_window != 0);
244  auto listPanels = dynamic_cast<PluginListPanel*>(
245  main_window->GetRealParent()->GetPrevSibling());
246  wxASSERT(listPanels != 0);
247  listPanels->ReloadPluginPanels();
248  auto window = GetSizer()->GetItem((size_t)0)->GetWindow();
249  auto btn = dynamic_cast<wxButton*>(window);
250  wxASSERT(btn != 0);
251  btn->SetLabel(_("Reinstall"));
252  }
253  }
254 
255 private:
256  PluginMetadata m_metadata;
257  bool m_remove;
258 
259  const char* getUpdateLabel(PlugInContainer* pic, PluginMetadata metadata) {
260  SemanticVersion currentVersion(pic->m_version_major, pic->m_version_minor);
261  if (pic->m_version_str != "") {
262  currentVersion = SemanticVersion::parse(pic->m_version_str.ToStdString());
263  }
264  auto newVersion = SemanticVersion::parse(metadata.version);
265  if (newVersion > currentVersion) {
266  return _("Update");
267  } else if (newVersion == currentVersion) {
268  return _("Reinstall");
269  } else {
270  return _("Downgrade");
271  }
272  }
273 };
274 
276 class CandidateButtonsPanel : public wxPanel {
277 public:
278  CandidateButtonsPanel(wxWindow* parent, const PluginMetadata* plugin)
279  : wxPanel(parent) {
280  auto flags = wxSizerFlags().Border();
281 
282  auto vbox = new wxBoxSizer(wxVERTICAL);
283  vbox->Add(new InstallButton(this, *plugin),
284  flags.DoubleBorder().Top().Right());
285  vbox->Add(1, 1, 1, wxEXPAND); // Expanding, stretchable spacer
286  m_info_btn = new WebsiteButton(this, plugin->info_url.c_str());
287  m_info_btn->Hide();
288  vbox->Add(m_info_btn, flags.DoubleBorder().Bottom().Right());
289  SetSizer(vbox);
290  Fit();
291  }
292 
293  void HideDetails(bool hide) {
294  m_info_btn->Show(!hide);
295  GetParent()->Layout();
296  }
297 
298 private:
299  WebsiteButton* m_info_btn;
300 };
301 
303 class PluginTextPanel : public wxPanel {
304 public:
305  PluginTextPanel(wxWindow* parent, const PluginMetadata* plugin,
306  CandidateButtonsPanel* buttons, bool bshowTuple = false)
307  : wxPanel(parent), m_descr(0), m_buttons(buttons) {
308  auto flags = wxSizerFlags().Border();
309 
310  MORE = "<span foreground=\'blue\'>";
311  MORE += _("More");
312  MORE += "...</span>";
313  LESS = "<span foreground=\'blue\'>";
314  LESS += _("Less");
315  LESS += "...</span>";
316 
317  auto sum_hbox = new wxBoxSizer(wxHORIZONTAL);
318  m_summary = staticText(plugin->summary);
319  sum_hbox->Add(m_summary);
320  sum_hbox->AddSpacer(10);
321  m_more = staticText("");
322  m_more->SetLabelMarkup(MORE);
323  sum_hbox->Add(m_more, wxSizerFlags());
324 
325  auto vbox = new wxBoxSizer(wxVERTICAL);
326  wxString nameText(plugin->name + " " + plugin->version);
327  if (bshowTuple) nameText += " " + plugin->target;
328  auto name = staticText(nameText);
329  m_descr = staticText(plugin->description);
330  m_descr->Hide();
331  vbox->Add(name, flags);
332  vbox->Add(sum_hbox, flags);
333  vbox->Add(m_descr, flags.Expand());
334  SetSizer(vbox);
335 
336  m_more->Bind(wxEVT_LEFT_DOWN, &PluginTextPanel::OnClick, this);
337  m_descr->Bind(wxEVT_LEFT_DOWN, &PluginTextPanel::OnClick, this);
338  }
339 
340  void OnClick(wxMouseEvent& event) {
341  m_descr->Show(!m_descr->IsShown());
342  m_more->SetLabelMarkup(m_descr->IsShown() ? LESS : MORE);
343  m_buttons->HideDetails(!m_descr->IsShown());
344  GetParent()->SendSizeEvent();
345  GetParent()->GetParent()->GetParent()->Layout();
346  GetParent()->GetParent()->GetParent()->Refresh(true);
347  GetParent()->GetParent()->GetParent()->Update();
348  }
349 
350 protected:
351  wxString MORE, LESS;
352 
353  wxStaticText* staticText(const wxString& text) {
354  return new wxStaticText(this, wxID_ANY, text, wxDefaultPosition,
355  wxDefaultSize, wxALIGN_LEFT);
356  }
357 
358  wxStaticText* m_descr;
359  wxStaticText* m_more;
360  wxStaticText* m_summary;
361  CandidateButtonsPanel* m_buttons;
362 };
363 
367 class MainButtonsPanel : public wxPanel {
368 public:
369  MainButtonsPanel(wxWindow* parent, wxWindow* victim)
370  : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(200, 32)) {
371  auto sizer = new wxBoxSizer(wxHORIZONTAL);
372  auto spacing = GetTextExtent("m").GetWidth();
373  sizer->Add(1, 1, 1, wxEXPAND); // Expanding, stretchable space
374  sizer->Add(new UpdateCatalogDialogBtn(this), wxSizerFlags());
375  sizer->Add(spacing, 0);
376  sizer->Add(new UpdateCatalogNowBtn(this), wxSizerFlags());
377  sizer->Add(spacing, 0);
378  sizer->Add(new wxButton(this, wxID_OK, _("Done")), wxSizerFlags());
379  SetSizer(sizer);
380  }
381 
382 protected:
384  class UpdateCatalogNowBtn : public wxButton {
385  public:
386  UpdateCatalogNowBtn(wxWindow* parent)
387  : wxButton(parent, wxID_ANY, _("Update plugin catalog")) {
388  Bind(wxEVT_COMMAND_BUTTON_CLICKED, [=](wxCommandEvent&) {
389  new SimpleCatalogDialog(this);
390  wxCommandEvent evt(EVT_PLUGINS_RELOAD);
391  wxPostEvent(GetParent(), evt);
392  });
393  }
394  };
395 
397  class UpdateCatalogDialogBtn : public wxButton {
398  public:
399  UpdateCatalogDialogBtn(wxWindow* parent)
400  : wxButton(parent, wxID_ANY, _("Advanced catalog update...")) {
401  Bind(wxEVT_COMMAND_BUTTON_CLICKED,
402  [=](wxCommandEvent&) { new AdvancedCatalogDialog(this); });
403  }
404  };
405 };
406 
411 class OcpnScrolledWindow : public wxScrolledWindow {
412 public:
413  OcpnScrolledWindow(wxWindow* parent)
414  : wxScrolledWindow(parent), m_grid(new wxFlexGridSizer(3, 0, 0)) {
415  auto box = new wxBoxSizer(wxVERTICAL);
416  populateGrid(m_grid);
417  box->Add(m_grid, wxSizerFlags().Proportion(1).Expand());
418  auto button_panel = new MainButtonsPanel(this, parent);
419  box->Add(button_panel, wxSizerFlags().Right().Border().Expand());
420  Bind(EVT_PLUGINS_RELOAD, [&](wxCommandEvent& ev) { Reload(); });
421 
422  SetSizer(box);
423  FitInside();
424  // TODO: Compute size using wxWindow::GetEffectiveMinSize()
425  SetScrollRate(1, 1);
426  };
427 
428  void populateGrid(wxFlexGridSizer* grid) {
430  struct metadata_compare {
431  bool operator()(const PluginMetadata& lhs,
432  const PluginMetadata& rhs) const {
433  return lhs.key() < rhs.key();
434  }
435  };
436 
437  auto flags = wxSizerFlags();
438  grid->SetCols(3);
439  grid->AddGrowableCol(2);
440  auto available = PluginHandler::getInstance()->getAvailable();
441  std::set<PluginMetadata, metadata_compare> unique_plugins;
442  for (auto plugin : PluginHandler::getInstance()->getAvailable()) {
443  unique_plugins.insert(plugin);
444  }
445  for (auto plugin : unique_plugins) {
446  if (!PluginHandler::isCompatible(plugin)) {
447  continue;
448  }
449  grid->Add(new PluginIconPanel(this, plugin.name), flags.Expand());
450  auto buttons = new CandidateButtonsPanel(this, &plugin);
451  grid->Add(new PluginTextPanel(this, &plugin, buttons,
452  unique_plugins.size() > 1),
453  flags.Proportion(1).Right());
454  grid->Add(buttons, flags.DoubleBorder());
455  grid->Add(new wxStaticLine(this), wxSizerFlags(0).Expand());
456  grid->Add(new wxStaticLine(this), wxSizerFlags(0).Expand());
457  grid->Add(new wxStaticLine(this), wxSizerFlags(0).Expand());
458  }
459  }
460 
461  void Reload() {
462  Hide();
463  m_grid->Clear();
464  populateGrid(m_grid);
465  Layout();
466  Show();
467  FitInside();
468  Refresh(true);
469  }
470 
471 private:
472  wxFlexGridSizer* m_grid;
473 };
474 
475 } // namespace download_mgr
476 
479  : wxDialog(parent, wxID_ANY, _("Plugin Manager"), wxDefaultPosition,
480  wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
481  auto vbox = new wxBoxSizer(wxVERTICAL);
482  auto scrwin = new download_mgr::OcpnScrolledWindow(this);
483  vbox->Add(scrwin, wxSizerFlags(1).Expand());
484 
485  // The list has no natural height. Allocate 20 lines of text so some
486  // items are displayed initially in Layout()
487  int min_height = GetTextExtent("abcdefghijklmnopqrst").GetHeight() * 20;
488 
489  // There seem to be no way have dynamic, wrapping text:
490  // https://forums.wxwidgets.org/viewtopic.php?f=1&t=46662
491  int width = GetParent()->GetClientSize().GetWidth();
492  SetMinClientSize(wxSize(width, min_height));
493 
494  SetSizer(vbox);
495  Fit();
496  Layout();
497 }
498 
501  : Downloader(plugin.tarball_url),
502  m_downloaded(0),
503  m_dialog(0),
504  m_plugin(plugin),
505  m_parent(parent) {}
506 
507 std::string GuiDownloader::run(wxWindow* parent, bool remove_current) {
508  bool ok;
509  bool downloaded = false;
510  std::string path = ocpn::lookup_tarball(m_plugin.tarball_url.c_str());
511  if (!path.size()) {
512  long size = get_filesize();
513  std::string label(_("Downloading "));
514  label += url;
515  m_dialog =
516  new wxProgressDialog(_("Downloading"), label.c_str(), size, parent,
517  wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT);
518 #ifdef __OCPN__ANDROID__
519  m_dialog->SetBackgroundColour(wxColour(0x7c, 0xb0, 0xe9)); // light blue
520 #endif
521 
522  ok = download(path);
523  g_Platform->HideBusySpinner();
524 
525  if (!ok) {
526  delete m_dialog;
527  showErrorDialog("Download error");
528  return "";
529  }
530 
531  // Download aborted?
532  if (m_dialog == 0) {
533  showErrorDialog("Download aborted");
534  return "";
535  } else {
536  delete m_dialog;
537  }
538 
539  if (!download_mgr::checksum_ok(path, m_plugin)) {
540  showErrorDialog("Checksum error");
541  return "";
542  }
543 
544  m_dialog = 0; // make sure that on_chunk() doesn't misbehave.
545  downloaded = true;
546  }
547 
548  auto pluginHandler = PluginHandler::getInstance();
549  if (remove_current) {
550  wxLogMessage("Uninstalling %s", m_plugin.name.c_str());
551  pluginHandler->uninstall(m_plugin.name);
552  }
553  ok = pluginHandler->installPlugin(m_plugin, path);
554  if (!ok) {
555  showErrorDialog("Installation error");
556  return "";
557  }
558 
559  if (downloaded) {
560  // Cache the tarball from the tmp location to the plugin cache.
561  wxURI uri(wxString(m_plugin.tarball_url.c_str()));
562  wxFileName fn(uri.GetPath());
563  auto basename = fn.GetFullName().ToStdString();
564  if (ocpn::store_tarball(path.c_str(), basename.c_str())) {
565  wxLogMessage("Copied %s to local cache at %s", path.c_str(),
566  basename.c_str());
567  }
568  }
569 
570  wxMessageDialog* dlg = new wxMessageDialog(
571  m_parent,
572  m_plugin.name + " " + m_plugin.version + _(" successfully installed"),
573  _("Installation complete"), wxOK | wxCENTRE | wxICON_INFORMATION);
574  dlg->ShowModal();
575  return path;
576 }
577 
578 void GuiDownloader::on_chunk(const char* buff, unsigned bytes) {
579  Downloader::on_chunk(buff, bytes);
580  m_downloaded += bytes;
581  if (m_dialog && !m_dialog->Update(m_downloaded)) {
582  // User pushed Cancel button
583  delete m_dialog;
584  m_dialog = 0;
585  }
586 }
587 
588 void GuiDownloader::showErrorDialog(const char* msg) {
589  auto dlg = new wxMessageDialog(m_parent, "", _("Installation error"),
590  wxOK | wxICON_ERROR);
591  auto last_error_msg = last_error();
592  std::string text = msg;
593  if (last_error_msg != "") {
594  text = text + ": " + error_msg;
595  }
596  text = text + "\nPlease check system log for more info.";
597  dlg->SetMessage(text);
598  dlg->ShowModal();
599  dlg->Destroy();
600 }
Catalog handler GUI.
Definition: catalog_mgr.h:33
Handle downloading of files from remote urls.
Definition: downloader.h:34
bool download(std::ostream *stream)
Download url into stream, return false on errors.
Definition: downloader.cpp:55
long get_filesize()
Try to get remote filesize, return 0 on failure.
Definition: downloader.cpp:99
std::string last_error()
Last Curl error message.
Definition: downloader.cpp:49
virtual void on_chunk(const char *buff, unsigned bytes)
Called when given bytes has been transferred from remote.
Definition: downloader.cpp:51
Add progress and final message dialogs to the basic Downloader.
Definition: download_mgr.h:47
void on_chunk(const char *buff, unsigned bytes) override
Called when given bytes has been transferred from remote.
GuiDownloader(wxWindow *parent, PluginMetadata plugin)
Add progress and final message dialogs to the basic Downloader.
Data for a loaded plugin, including dl-loaded library.
Definition: plugin_loader.h:99
wxString m_version_str
Complete version as of semantic_vers.
Definition: plugin_loader.h:88
PluginDownloadDialog(wxWindow *parent)
Top-level install plugins dialog.
bool uninstall(const std::string plugin)
Uninstall an installed and loaded plugin.
const std::vector< PluginMetadata > getAvailable()
Update catalog and return list of available, not installed plugins.
static bool isCompatible(const PluginMetadata &metadata, const char *os=PKG_TARGET, const char *os_version=PKG_TARGET_VERSION)
Return true if given plugin is loadable on given os/version.
Invokes client browser on plugin info_url when clicked.
The two buttons 'install' and 'website', the latter optionally hidden.
Download and install a PluginMetadata item when clicked.
Button invoking the advanced catalog dialog.
Invokes the simple update catalog procedure.
Three buttons bottom-right for plugin catalog maintenance.
The list of download candidates in a scrolled window + OK and Settings button.
void populateGrid(wxFlexGridSizer *grid)
A plugin icon, scaled to about 2/3 of available space.
Plugin name, version, summary + an optionally shown description.
std::string lookup_tarball(const char *uri)
Get path to tarball in cache for given filename.
bool store_tarball(const char *path, const char *basename)
Store a tarball in tarball cache, return success/fail.
wxDEFINE_EVENT(REST_IO_EVT, ObservedEvt)
Event from IO thread to main.
Plugin metadata, reflects the xml format directly.
Versions uses a modified semantic versioning scheme: major.minor.revision.post-tag+build.
Definition: semantic_vers.h:51
static SemanticVersion parse(std::string s)
Parse a version string, sets major == -1 on errors.
Runtime representation of a plugin block.