mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2025-07-20 03:35:46 +00:00
[desktop] feat: install firmware from ZIP (#52)
Closes #12 Adds a menu option to install firmware from a packed ZIP. This PR additionally lays the groundwork to add data import/export via ZIP. In the future, a qt_common subproject should be added to handle common Qt tasks such as this. Furthermore, to decrease dependency complexity, this also introduces CPM, a wrapper around FetchContent. In theory, this should also lay the groundwork for #8 as well. Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/52
This commit is contained in:
parent
7b23cd0df4
commit
f99488fe3e
8 changed files with 252 additions and 32 deletions
80
.ci/patch/0001-quazip-strict.patch
Normal file
80
.ci/patch/0001-quazip-strict.patch
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
diff --git a/quazip/quazipdir.cpp b/quazip/quazipdir.cpp
|
||||||
|
index d43f1c1..eb24bf1 100644
|
||||||
|
--- a/quazip/quazipdir.cpp
|
||||||
|
+++ b/quazip/quazipdir.cpp
|
||||||
|
@@ -293,8 +293,8 @@ bool QuaZipDirComparator::operator()(const QuaZipFileInfo64 &info1,
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename TFileInfoList>
|
||||||
|
-bool QuaZipDirPrivate::entryInfoList(QStringList nameFilters,
|
||||||
|
- QDir::Filters filter, QDir::SortFlags sort, TFileInfoList &result) const
|
||||||
|
+bool QuaZipDirPrivate::entryInfoList(QStringList _nameFilters,
|
||||||
|
+ QDir::Filters _filter, QDir::SortFlags sort, TFileInfoList &result) const
|
||||||
|
{
|
||||||
|
QString basePath = simplePath();
|
||||||
|
if (!basePath.isEmpty())
|
||||||
|
@@ -305,12 +305,12 @@ bool QuaZipDirPrivate::entryInfoList(QStringList nameFilters,
|
||||||
|
if (!zip->goToFirstFile()) {
|
||||||
|
return zip->getZipError() == UNZ_OK;
|
||||||
|
}
|
||||||
|
- QDir::Filters fltr = filter;
|
||||||
|
+ QDir::Filters fltr = _filter;
|
||||||
|
if (fltr == QDir::NoFilter)
|
||||||
|
fltr = this->filter;
|
||||||
|
if (fltr == QDir::NoFilter)
|
||||||
|
fltr = QDir::AllEntries;
|
||||||
|
- QStringList nmfltr = nameFilters;
|
||||||
|
+ QStringList nmfltr = _nameFilters;
|
||||||
|
if (nmfltr.isEmpty())
|
||||||
|
nmfltr = this->nameFilters;
|
||||||
|
QSet<QString> dirsFound;
|
||||||
|
diff --git a/quazip/quazipfile.cpp b/quazip/quazipfile.cpp
|
||||||
|
index 4a5f2f9..f7865f5 100644
|
||||||
|
--- a/quazip/quazipfile.cpp
|
||||||
|
+++ b/quazip/quazipfile.cpp
|
||||||
|
@@ -241,14 +241,14 @@ void QuaZipFile::setFileName(const QString& fileName, QuaZip::CaseSensitivity cs
|
||||||
|
p->caseSensitivity=cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
-void QuaZipFilePrivate::setZipError(int zipError) const
|
||||||
|
+void QuaZipFilePrivate::setZipError(int _zipError) const
|
||||||
|
{
|
||||||
|
QuaZipFilePrivate *fakeThis = const_cast<QuaZipFilePrivate*>(this); // non-const
|
||||||
|
- fakeThis->zipError=zipError;
|
||||||
|
- if(zipError==UNZ_OK)
|
||||||
|
+ fakeThis->zipError = _zipError;
|
||||||
|
+ if(_zipError == UNZ_OK)
|
||||||
|
q->setErrorString(QString());
|
||||||
|
else
|
||||||
|
- q->setErrorString(QuaZipFile::tr("ZIP/UNZIP API error %1").arg(zipError));
|
||||||
|
+ q->setErrorString(QuaZipFile::tr("ZIP/UNZIP API error %1").arg(_zipError));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QuaZipFile::open(OpenMode mode)
|
||||||
|
diff --git a/quazip/unzip.c b/quazip/unzip.c
|
||||||
|
index a39365d..ee7b487 100644
|
||||||
|
--- a/quazip/unzip.c
|
||||||
|
+++ b/quazip/unzip.c
|
||||||
|
@@ -1054,7 +1054,7 @@ local int unz64local_GetCurrentFileInfoInternal (unzFile file,
|
||||||
|
/* ZIP64 extra fields */
|
||||||
|
if (headerId == 0x0001)
|
||||||
|
{
|
||||||
|
- uLong uL;
|
||||||
|
+ uLong _uL;
|
||||||
|
|
||||||
|
if(file_info.uncompressed_size == (ZPOS64_T)0xFFFFFFFFu)
|
||||||
|
{
|
||||||
|
@@ -1078,7 +1078,7 @@ local int unz64local_GetCurrentFileInfoInternal (unzFile file,
|
||||||
|
if(file_info.disk_num_start == 0xFFFFFFFFu)
|
||||||
|
{
|
||||||
|
/* Disk Start Number */
|
||||||
|
- if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)
|
||||||
|
+ if (unz64local_getLong(&s->z_filefunc, s->filestream, &_uL) != UNZ_OK)
|
||||||
|
err=UNZ_ERRNO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -2151,3 +2151,4 @@ int ZEXPORT unzClearFlags(unzFile file, unsigned flags)
|
||||||
|
s->flags &= ~flags;
|
||||||
|
return UNZ_OK;
|
||||||
|
}
|
||||||
|
+
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -52,4 +52,3 @@ Thumbs.db
|
||||||
eden-windows-msvc
|
eden-windows-msvc
|
||||||
artifacts
|
artifacts
|
||||||
*.AppImage*
|
*.AppImage*
|
||||||
*.patch
|
|
||||||
|
|
24
CMakeModules/CPM.cmake
Normal file
24
CMakeModules/CPM.cmake
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
#
|
||||||
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors
|
||||||
|
|
||||||
|
set(CPM_DOWNLOAD_VERSION 0.42.0)
|
||||||
|
set(CPM_HASH_SUM "2020b4fc42dba44817983e06342e682ecfc3d2f484a581f11cc5731fbe4dce8a")
|
||||||
|
|
||||||
|
if(CPM_SOURCE_CACHE)
|
||||||
|
set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
|
||||||
|
elseif(DEFINED ENV{CPM_SOURCE_CACHE})
|
||||||
|
set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
|
||||||
|
else()
|
||||||
|
set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Expand relative path. This is important if the provided path contains a tilde (~)
|
||||||
|
get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE)
|
||||||
|
|
||||||
|
file(DOWNLOAD
|
||||||
|
https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake
|
||||||
|
${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM}
|
||||||
|
)
|
||||||
|
|
||||||
|
include(${CPM_DOWNLOAD_LOCATION})
|
|
@ -494,4 +494,25 @@ if (YUZU_ROOM)
|
||||||
target_link_libraries(yuzu PRIVATE yuzu-room)
|
target_link_libraries(yuzu PRIVATE yuzu-room)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Extra deps
|
||||||
|
set(BUILD_SHARED_LIBS OFF)
|
||||||
|
|
||||||
|
include(CPM)
|
||||||
|
set(CPM_SOURCE_CACHE ${CMAKE_SOURCE_DIR}/.cache/cpm)
|
||||||
|
|
||||||
|
CPMAddPackage(
|
||||||
|
URI "gh:stachenov/quazip@1.5"
|
||||||
|
PATCHES
|
||||||
|
${CMAKE_SOURCE_DIR}/.ci/patch/0001-quazip-strict.patch
|
||||||
|
)
|
||||||
|
|
||||||
|
if (NOT MSVC)
|
||||||
|
target_compile_options(QuaZip PRIVATE
|
||||||
|
-Wno-error=shadow
|
||||||
|
-Wno-error=missing-declarations
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(yuzu PRIVATE QuaZip::QuaZip)
|
||||||
|
|
||||||
create_target_directory_groups(yuzu)
|
create_target_directory_groups(yuzu)
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
#include "core/tools/renderdoc.h"
|
#include "core/tools/renderdoc.h"
|
||||||
#include "frontend_common/firmware_manager.h"
|
#include "frontend_common/firmware_manager.h"
|
||||||
|
|
||||||
|
#include <JlCompress.h>
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
#include <unistd.h> // for chdir
|
#include <unistd.h> // for chdir
|
||||||
#endif
|
#endif
|
||||||
|
@ -1683,7 +1685,8 @@ void GMainWindow::ConnectMenuEvents() {
|
||||||
|
|
||||||
connect_menu(ui->action_Discord, &GMainWindow::OnOpenDiscord);
|
connect_menu(ui->action_Discord, &GMainWindow::OnOpenDiscord);
|
||||||
connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents);
|
connect_menu(ui->action_Verify_installed_contents, &GMainWindow::OnVerifyInstalledContents);
|
||||||
connect_menu(ui->action_Install_Firmware, &GMainWindow::OnInstallFirmware);
|
connect_menu(ui->action_Firmware_From_Folder, &GMainWindow::OnInstallFirmware);
|
||||||
|
connect_menu(ui->action_Firmware_From_ZIP, &GMainWindow::OnInstallFirmwareFromZIP);
|
||||||
connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys);
|
connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys);
|
||||||
connect_menu(ui->action_About, &GMainWindow::OnAbout);
|
connect_menu(ui->action_About, &GMainWindow::OnAbout);
|
||||||
}
|
}
|
||||||
|
@ -1714,7 +1717,8 @@ void GMainWindow::UpdateMenuState() {
|
||||||
action->setEnabled(emulation_running);
|
action->setEnabled(emulation_running);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui->action_Install_Firmware->setEnabled(!emulation_running);
|
ui->action_Firmware_From_Folder->setEnabled(!emulation_running);
|
||||||
|
ui->action_Firmware_From_ZIP->setEnabled(!emulation_running);
|
||||||
ui->action_Install_Keys->setEnabled(!emulation_running);
|
ui->action_Install_Keys->setEnabled(!emulation_running);
|
||||||
|
|
||||||
for (QAction* action : applet_actions) {
|
for (QAction* action : applet_actions) {
|
||||||
|
@ -4239,26 +4243,8 @@ void GMainWindow::OnVerifyInstalledContents() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::OnInstallFirmware() {
|
void GMainWindow::InstallFirmware(const QString &location, bool recursive)
|
||||||
// Don't do this while emulation is running, that'd probably be a bad idea.
|
{
|
||||||
if (emu_thread != nullptr && emu_thread->IsRunning()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for installed keys, error out, suggest restart?
|
|
||||||
if (!ContentManager::AreKeysPresent()) {
|
|
||||||
QMessageBox::information(
|
|
||||||
this, tr("Keys not installed"),
|
|
||||||
tr("Install decryption keys and restart eden before attempting to install firmware."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString firmware_source_location = QFileDialog::getExistingDirectory(
|
|
||||||
this, tr("Select Dumped Firmware Source Location"), {}, QFileDialog::ShowDirsOnly);
|
|
||||||
if (firmware_source_location.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QProgressDialog progress(tr("Installing Firmware..."), tr("Cancel"), 0, 100, this);
|
QProgressDialog progress(tr("Installing Firmware..."), tr("Cancel"), 0, 100, this);
|
||||||
progress.setWindowModality(Qt::WindowModal);
|
progress.setWindowModality(Qt::WindowModal);
|
||||||
progress.setMinimumDuration(100);
|
progress.setMinimumDuration(100);
|
||||||
|
@ -4272,11 +4258,11 @@ void GMainWindow::OnInstallFirmware() {
|
||||||
return progress.wasCanceled();
|
return progress.wasCanceled();
|
||||||
};
|
};
|
||||||
|
|
||||||
LOG_INFO(Frontend, "Installing firmware from {}", firmware_source_location.toStdString());
|
LOG_INFO(Frontend, "Installing firmware from {}", location.toStdString());
|
||||||
|
|
||||||
// Check for a reasonable number of .nca files (don't hardcode them, just see if there's some in
|
// Check for a reasonable number of .nca files (don't hardcode them, just see if there's some in
|
||||||
// there.)
|
// there.)
|
||||||
std::filesystem::path firmware_source_path = firmware_source_location.toStdString();
|
std::filesystem::path firmware_source_path = location.toStdString();
|
||||||
if (!Common::FS::IsDir(firmware_source_path)) {
|
if (!Common::FS::IsDir(firmware_source_path)) {
|
||||||
progress.close();
|
progress.close();
|
||||||
return;
|
return;
|
||||||
|
@ -4294,7 +4280,12 @@ void GMainWindow::OnInstallFirmware() {
|
||||||
|
|
||||||
QtProgressCallback(100, 10);
|
QtProgressCallback(100, 10);
|
||||||
|
|
||||||
Common::FS::IterateDirEntries(firmware_source_path, callback, Common::FS::DirEntryFilter::File);
|
if (recursive) {
|
||||||
|
Common::FS::IterateDirEntriesRecursively(firmware_source_path, callback, Common::FS::DirEntryFilter::File);
|
||||||
|
} else {
|
||||||
|
Common::FS::IterateDirEntries(firmware_source_path, callback, Common::FS::DirEntryFilter::File);
|
||||||
|
}
|
||||||
|
|
||||||
if (out.size() <= 0) {
|
if (out.size() <= 0) {
|
||||||
progress.close();
|
progress.close();
|
||||||
QMessageBox::warning(this, tr("Firmware install failed"),
|
QMessageBox::warning(this, tr("Firmware install failed"),
|
||||||
|
@ -4377,6 +4368,93 @@ void GMainWindow::OnInstallFirmware() {
|
||||||
OnCheckFirmware();
|
OnCheckFirmware();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GMainWindow::OnInstallFirmware() {
|
||||||
|
// Don't do this while emulation is running, that'd probably be a bad idea.
|
||||||
|
if (emu_thread != nullptr && emu_thread->IsRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for installed keys, error out, suggest restart?
|
||||||
|
if (!ContentManager::AreKeysPresent()) {
|
||||||
|
QMessageBox::information(
|
||||||
|
this, tr("Keys not installed"),
|
||||||
|
tr("Install decryption keys and restart Eden before attempting to install firmware."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString firmware_source_location = QFileDialog::getExistingDirectory(
|
||||||
|
this, tr("Select Dumped Firmware Source Location"), {}, QFileDialog::ShowDirsOnly);
|
||||||
|
if (firmware_source_location.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallFirmware(firmware_source_location);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GMainWindow::OnInstallFirmwareFromZIP()
|
||||||
|
{
|
||||||
|
// Don't do this while emulation is running, that'd probably be a bad idea.
|
||||||
|
if (emu_thread != nullptr && emu_thread->IsRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for installed keys, error out, suggest restart?
|
||||||
|
if (!ContentManager::AreKeysPresent()) {
|
||||||
|
QMessageBox::information(
|
||||||
|
this, tr("Keys not installed"),
|
||||||
|
tr("Install decryption keys and restart Eden before attempting to install firmware."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString firmware_zip_location = QFileDialog::getOpenFileName(
|
||||||
|
this, tr("Select Dumped Firmware ZIP"), {}, tr("Zipped Archives (*.zip)"));
|
||||||
|
if (firmware_zip_location.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
fs::path tmp{std::filesystem::temp_directory_path()};
|
||||||
|
|
||||||
|
if (!std::filesystem::create_directories(tmp / "eden" / "firmware")) {
|
||||||
|
goto unzipFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
tmp /= "eden";
|
||||||
|
tmp /= "firmware";
|
||||||
|
|
||||||
|
QString qCacheDir = QString::fromStdString(tmp.string());
|
||||||
|
|
||||||
|
QFile zip(firmware_zip_location);
|
||||||
|
|
||||||
|
QStringList result = JlCompress::extractDir(&zip, qCacheDir);
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
goto unzipFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In this case, it has to be done recursively, since sometimes people
|
||||||
|
// will pack it into a subdirectory after dumping
|
||||||
|
InstallFirmware(qCacheDir, true);
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove_all(tmp, ec);
|
||||||
|
|
||||||
|
if (ec) {
|
||||||
|
QMessageBox::warning(this, tr("Firmware cleanup failed"),
|
||||||
|
tr("Failed to clean up extracted firmware cache.\n"
|
||||||
|
"Check write permissions in the system temp directory and try again.\nOS reported error: %1")
|
||||||
|
.arg(ec.message()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unzipFailed:
|
||||||
|
QMessageBox::critical(this, tr("Firmware unzip failed"),
|
||||||
|
tr("Check write permissions in the system temp directory and try again."));
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void GMainWindow::OnInstallDecryptionKeys() {
|
void GMainWindow::OnInstallDecryptionKeys() {
|
||||||
// Don't do this while emulation is running.
|
// Don't do this while emulation is running.
|
||||||
if (emu_thread != nullptr && emu_thread->IsRunning()) {
|
if (emu_thread != nullptr && emu_thread->IsRunning()) {
|
||||||
|
|
|
@ -394,6 +394,7 @@ private slots:
|
||||||
void OnOpenLogFolder();
|
void OnOpenLogFolder();
|
||||||
void OnVerifyInstalledContents();
|
void OnVerifyInstalledContents();
|
||||||
void OnInstallFirmware();
|
void OnInstallFirmware();
|
||||||
|
void OnInstallFirmwareFromZIP();
|
||||||
void OnInstallDecryptionKeys();
|
void OnInstallDecryptionKeys();
|
||||||
void OnAbout();
|
void OnAbout();
|
||||||
void OnToggleFilterBar();
|
void OnToggleFilterBar();
|
||||||
|
@ -614,6 +615,8 @@ private:
|
||||||
std::string arguments,
|
std::string arguments,
|
||||||
const bool needs_title);
|
const bool needs_title);
|
||||||
|
|
||||||
|
void InstallFirmware(const QString& location, bool recursive = false);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void dropEvent(QDropEvent* event) override;
|
void dropEvent(QDropEvent* event) override;
|
||||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
void dragEnterEvent(QDragEnterEvent* event) override;
|
||||||
|
|
|
@ -182,8 +182,15 @@
|
||||||
<addaction name="action_Desktop"/>
|
<addaction name="action_Desktop"/>
|
||||||
<addaction name="action_Application_Menu"/>
|
<addaction name="action_Application_Menu"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QMenu" name="menuInstall_Firmware">
|
||||||
|
<property name="title">
|
||||||
|
<string>Install Firmware</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="action_Firmware_From_Folder"/>
|
||||||
|
<addaction name="action_Firmware_From_ZIP"/>
|
||||||
|
</widget>
|
||||||
<addaction name="action_Install_Keys"/>
|
<addaction name="action_Install_Keys"/>
|
||||||
<addaction name="action_Install_Firmware"/>
|
<addaction name="menuInstall_Firmware"/>
|
||||||
<addaction name="action_Verify_installed_contents"/>
|
<addaction name="action_Verify_installed_contents"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="menu_cabinet_applet"/>
|
<addaction name="menu_cabinet_applet"/>
|
||||||
|
@ -484,11 +491,6 @@
|
||||||
<string>Open &Controller Menu</string>
|
<string>Open &Controller Menu</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="action_Install_Firmware">
|
|
||||||
<property name="text">
|
|
||||||
<string>Install Firmware</string>
|
|
||||||
</property>
|
|
||||||
</action>
|
|
||||||
<action name="action_Install_Keys">
|
<action name="action_Install_Keys">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Install Decryption Keys</string>
|
<string>Install Decryption Keys</string>
|
||||||
|
@ -545,6 +547,16 @@
|
||||||
<action name="action_Log_Folder">
|
<action name="action_Log_Folder">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Log Folder</string>
|
<string>&Log Folder</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="action_Firmware_From_Folder">
|
||||||
|
<property name="text">
|
||||||
|
<string>From Folder</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="action_Firmware_From_ZIP">
|
||||||
|
<property name="text">
|
||||||
|
<string>From ZIP</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
|
|
3
tools/update-cpm.sh
Executable file
3
tools/update-cpm.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
wget -O CMakeModules/CPM.cmake https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake
|
Loading…
Add table
Add a link
Reference in a new issue