From 2ec22e9be83dd2fcba7e0670d823d3d6c1e98a0b Mon Sep 17 00:00:00 2001
From: TableFlipper9 <
[email protected]>
Date: Fri, 1 Aug 2025 12:00:16 +0300
Subject: [PATCH] Calamares 3.3.14: introducing Gentoo Stage3 Chooser
* introduced stage3 choosing module
* created boxes to choose mirrors, arches and stage3 archives
* introduced simple warning to new user regarding
blank mirror, which means default mirror
* error showing for faild fetches or network problems
Signed-off-by: Morovan Mihai <
[email protected]>
---
src/modules/stagechoose/CMakeLists.txt | 13 ++
src/modules/stagechoose/Config.cpp | 128 +++++++++++++
src/modules/stagechoose/Config.h | 65 +++++++
src/modules/stagechoose/SetStage3Job.cpp | 61 +++++++
src/modules/stagechoose/SetStage3Job.h | 22 +++
src/modules/stagechoose/StageChoosePage.cpp | 145 +++++++++++++++
src/modules/stagechoose/StageChoosePage.h | 51 ++++++
src/modules/stagechoose/StageChoosePage.ui | 172 ++++++++++++++++++
.../stagechoose/StageChooseViewStep.cpp | 80 ++++++++
src/modules/stagechoose/StageChooseViewStep.h | 53 ++++++
src/modules/stagechoose/StageFetcher.cpp | 152 ++++++++++++++++
src/modules/stagechoose/StageFetcher.h | 42 +++++
src/modules/stagechoose/stagechoose.conf | 7 +
.../stagechoose/stagechoose.schema.yaml | 17 ++
14 files changed, 1008 insertions(+)
create mode 100644 src/modules/stagechoose/CMakeLists.txt
create mode 100644 src/modules/stagechoose/Config.cpp
create mode 100644 src/modules/stagechoose/Config.h
create mode 100644 src/modules/stagechoose/SetStage3Job.cpp
create mode 100644 src/modules/stagechoose/SetStage3Job.h
create mode 100644 src/modules/stagechoose/StageChoosePage.cpp
create mode 100644 src/modules/stagechoose/StageChoosePage.h
create mode 100644 src/modules/stagechoose/StageChoosePage.ui
create mode 100644 src/modules/stagechoose/StageChooseViewStep.cpp
create mode 100644 src/modules/stagechoose/StageChooseViewStep.h
create mode 100644 src/modules/stagechoose/StageFetcher.cpp
create mode 100644 src/modules/stagechoose/StageFetcher.h
create mode 100644 src/modules/stagechoose/stagechoose.conf
create mode 100644 src/modules/stagechoose/stagechoose.schema.yaml
diff --git a/src/modules/stagechoose/CMakeLists.txt b/src/modules/stagechoose/CMakeLists.txt
new file mode 100644
index 0000000000..f1ee399f9e
--- /dev/null
+++ b/src/modules/stagechoose/CMakeLists.txt
@@ -0,0 +1,13 @@
+calamares_add_plugin(stagechoose
+ TYPE viewmodule
+ EXPORT_MACRO PLUGINDLLEXPORT_PRO
+ SOURCES
+ Config.cpp
+ StageChooseViewStep.cpp
+ StageChoosePage.cpp
+ SetStage3Job.cpp
+ StageFetcher.cpp
+ UI
+ StageChoosePage.ui
+ SHARED_LIB
+)
diff --git a/src/modules/stagechoose/Config.cpp b/src/modules/stagechoose/Config.cpp
new file mode 100644
index 0000000000..ac5feb3960
--- /dev/null
+++ b/src/modules/stagechoose/Config.cpp
@@ -0,0 +1,128 @@
+/* === This file is part of Calamares - <
https://calamares.io> ===
+ *
+ * SPDX-FileCopyrightText: 2019-2020 Adriaan de Groot <
[email protected]>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Calamares is Free Software: see the License-Identifier above.
+ *
+ */
+
+#include "Config.h"
+#include "locale/Global.h"
+#include "JobQueue.h"
+#include "GlobalStorage.h"
+#include "StageFetcher.h"
+
+#include <QDateTime>
+
+Config::Config(QObject* parent)
+ : QObject(parent)
+ , m_fetcher(new StageFetcher(this))
+{
+ connect(m_fetcher, &StageFetcher::variantsFetched, this, [this](const QStringList &variants) {
+ emit variantsReady(variants);
+ });
+
+ connect(m_fetcher, &StageFetcher::tarballFetched, this, [this](const QString &tarball) {
+ updateTarball(tarball);
+ });
+
+ connect(m_fetcher, &StageFetcher::fetchStatusChanged,this,&Config::fetchStatusChanged);
+ connect(m_fetcher, &StageFetcher::fetchError,this,&Config::fetchError);
+ /// change Config into function handles the fetcher signals
+ m_fetcher->setMirrorBase(m_mirrorBase);
+}
+
+QList<ArchitectureInfo> Config::availableArchitecturesInfo()
+{
+ QList<ArchitectureInfo> list;
+ list << ArchitectureInfo{ QStringLiteral("alpha"), QStringLiteral("Digital Alpha (alpha)") }
+ << ArchitectureInfo{ QStringLiteral("amd64"), QStringLiteral("64-bit Intel/AMD (amd64)") }
+ << ArchitectureInfo{ QStringLiteral("x86"), QStringLiteral("32-bit Intel/AMD (x86)") }
+ << ArchitectureInfo{ QStringLiteral("arm"), QStringLiteral("ARM 32-bit (arm)") }
+ << ArchitectureInfo{ QStringLiteral("arm64"), QStringLiteral("ARM 64-bit (arm64)") }
+ << ArchitectureInfo{ QStringLiteral("hppa"), QStringLiteral("HPPA (hppa)") }
+ << ArchitectureInfo{ QStringLiteral("ia64"), QStringLiteral("Intel Itanium (ia64)") }
+ << ArchitectureInfo{ QStringLiteral("loong"), QStringLiteral("Loongson MIPS-based (loong)") }
+ << ArchitectureInfo{ QStringLiteral("m68k"), QStringLiteral("Motorola 68k (m68k)") }
+ << ArchitectureInfo{ QStringLiteral("mips"), QStringLiteral("MIPS 32/64-bit (mips)") }
+ << ArchitectureInfo{ QStringLiteral("ppc"), QStringLiteral("PowerPC (ppc)") }
+ << ArchitectureInfo{ QStringLiteral("riscv"), QStringLiteral("RISC-V 32/64-bit (riscv)") }
+ << ArchitectureInfo{ QStringLiteral("s390"), QStringLiteral("IBM System z (s390)") }
+ << ArchitectureInfo{ QStringLiteral("sh"), QStringLiteral("SuperH legacy (sh)") }
+ << ArchitectureInfo{ QStringLiteral("sparc"), QStringLiteral("SPARC 64-bit (sparc)") }
+ << ArchitectureInfo{ QStringLiteral("livecd"), QStringLiteral("Live CD (unsafe)") };
+ return list;
+}
+
+void Config::availableStagesFor(const QString& arch)
+{
+ m_selectedArch = arch;
+ m_selectedVariant.clear();
+ if(arch == "livecd"){
+ m_fetcher->cancelOngoingRequest();
+ m_selectedTarball = "livecd";
+ emit tarballReady(m_selectedTarball);
+ emit fetchStatusChanged("LiveCD mode");
+ emit validityChanged(isValid());
+ return;
+ }
+ else{
+ m_selectedTarball.clear();
+ m_fetcher->fetchVariants(arch);
+ }
+}
+
+void Config::selectVariant(const QString& variant)
+{
+ m_selectedVariant = variant;
+
+ m_fetcher->fetchLatestTarball(m_selectedArch,variant);
+}
+
+QString Config::selectedStage3() const
+{
+ if(!m_selectedTarball.isEmpty())
+ return m_selectedTarball;
+
+ return "No tar fetched";
+}
+
+bool Config::isValid() const
+{
+ return (!m_selectedTarball.isEmpty()) ;
+}
+
+void Config::setMirrorBase(const QString& mirror){
+ QString base = mirror.trimmed();
+ while(base.endsWith('/')) base.chop(1);
+
+ if(base.isEmpty()) base = QStringLiteral("
http://distfiles.gentoo.org/releases");
+
+ if(base == m_mirrorBase) return;
+
+ m_mirrorBase = base;
+ if(m_fetcher) m_fetcher->setMirrorBase(m_mirrorBase);
+}
+
+void Config::updateTarball(const QString &tarball){
+ m_selectedTarball = tarball;
+ emit tarballReady(tarball);
+ emit validityChanged(isValid());
+}
+
+void Config::updateGlobalStorage()
+{
+ Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
+
+ if(m_selectedArch == "livecd")
+ gs->insert("GENTOO_LIVECD","yes");
+ else{
+ gs->insert("GENTOO_LIVECD","no");
+ gs->insert( "BASE_DOWNLOAD_URL", QString("%1/%2/autobuilds/%3/").arg(m_mirrorBase,m_selectedArch,m_selectedVariant));
+ gs->insert( "FINAL_DOWNLOAD_URL", QString("%1/%2/autobuilds/%3/%4").arg(m_mirrorBase,m_selectedArch,m_selectedVariant,m_selectedTarball));
+ gs->insert( "STAGE_NAME_TAR", m_selectedTarball );
+ }
+}
+
+
diff --git a/src/modules/stagechoose/Config.h b/src/modules/stagechoose/Config.h
new file mode 100644
index 0000000000..6ba116ed7e
--- /dev/null
+++ b/src/modules/stagechoose/Config.h
@@ -0,0 +1,65 @@
+/* === This file is part of Calamares - <
https://calamares.io> ===
+ *
+ * SPDX-FileCopyrightText: 2019-2020 Adriaan de Groot <
[email protected]>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Calamares is Free Software: see the License-Identifier above.
+ *
+ */
+
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#include "StageFetcher.h"
+#include <QObject>
+#include <QPair>
+#include <QString>
+#include <QList>
+
+struct ArchitectureInfo
+{
+ QString name;
+ QString description;
+
+ ArchitectureInfo() = default;
+ ArchitectureInfo(const QString& n, const QString& d):
+ name(n),description(d){}
+};
+
+class Config : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit Config(QObject* parent = nullptr);
+
+ QList<ArchitectureInfo> availableArchitecturesInfo();
+ QStringList availableArchitectures();
+ void availableStagesFor(const QString& architecture);
+ void selectVariant(const QString& variantKey);
+
+ QString selectedStage3() const;
+ bool isValid() const;
+
+ void updateGlobalStorage();
+ void updateTarball(const QString &tarball);
+ void setMirrorBase(const QString& mirror);
+ QString mirrorBase();
+
+signals:
+ void variantsReady(const QStringList& variants);
+ void tarballReady(const QString& tarball);
+ void fetchStatusChanged(const QString& status);
+ void fetchError(const QString& error);
+ void validityChanged(bool validity);
+
+private:
+ StageFetcher* m_fetcher;
+ QString m_mirrorBase {QStringLiteral("
http://distfiles.gentoo.org/releases")};
+ QString m_selectedArch;
+ QString m_selectedVariant;
+ QString m_selectedTarball;
+};
+
+#endif // CONFIG_H
+
diff --git a/src/modules/stagechoose/SetStage3Job.cpp b/src/modules/stagechoose/SetStage3Job.cpp
new file mode 100644
index 0000000000..086085862a
--- /dev/null
+++ b/src/modules/stagechoose/SetStage3Job.cpp
@@ -0,0 +1,61 @@
+#include "SetStage3Job.h"
+
+#include "utils/Logger.h"
+#include <QFile>
+#include <QTextStream>
+#include <QRegularExpression>
+
+SetStage3Job::SetStage3Job(const QString& tarballName)
+ : m_tarballName(tarballName)
+{
+}
+
+QString SetStage3Job::prettyName() const
+{
+ return QString("Write selected Gentoo Stage3 to config: %1").arg(m_tarballName);
+}
+
+Calamares::JobResult SetStage3Job::exec()
+{
+ if(m_tarballName.isEmpty()){
+ return Calamares::JobResult::error(
+ "No stage3 tarball selected.","Stage3 tarball name is empty."
+ );
+ }
+
+ QString configPath = "/etc/calamares.conf";
+ QFile file(configPath);
+ QString contents;
+
+ if (file.exists()) {
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return Calamares::JobResult::error(
+ "Failed to open Calamares config file for reading.",
+ configPath);
+ }
+ QTextStream in(&file);
+ contents = in.readAll();
+ file.close();
+ }
+
+ QString stage3Line = QString("stage3 = %1").arg(m_tarballName);
+
+ if (contents.contains(QRegularExpression(R"(stage3\s*=)"))) {
+ contents.replace(QRegularExpression(R"(stage3\s*=.*)"), stage3Line);
+ } else {
+ contents.append("\n" + stage3Line + "\n");
+ }
+
+ if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
+ return Calamares::JobResult::error(
+ "Failed to open Calamares config file for writing.",
+ configPath);
+ }
+
+ QTextStream out(&file);
+ out << contents;
+ file.close();
+
+ cDebug() << "Wrote stage3 tarball to config:" << m_tarballName;
+ return Calamares::JobResult::ok();
+}
diff --git a/src/modules/stagechoose/SetStage3Job.h b/src/modules/stagechoose/SetStage3Job.h
new file mode 100644
index 0000000000..edea12ce2c
--- /dev/null
+++ b/src/modules/stagechoose/SetStage3Job.h
@@ -0,0 +1,22 @@
+#ifndef SETSTAGE3JOB_H
+#define SETSTAGE3JOB_H
+
+#include <Job.h>
+#include <QString>
+
+/**
+ * @brief A job to write the selected Stage3 tarball name to /etc/calamares.conf
+ */
+class SetStage3Job : public Calamares::Job
+{
+public:
+ explicit SetStage3Job(const QString& tarballName);
+
+ QString prettyName() const override;
+ Calamares::JobResult exec() override;
+
+private:
+ QString m_tarballName;
+};
+
+#endif // SETSTAGE3JOB_H
diff --git a/src/modules/stagechoose/StageChoosePage.cpp b/src/modules/stagechoose/StageChoosePage.cpp
new file mode 100644
index 0000000000..5bf3eacb5e
--- /dev/null
+++ b/src/modules/stagechoose/StageChoosePage.cpp
@@ -0,0 +1,145 @@
+/* === This file is part of Calamares - <
https://calamares.io> ===
+ *
+ * SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <
[email protected]>
+ * SPDX-FileCopyrightText: 2015 Anke Boersma <
[email protected]>
+ * SPDX-FileCopyrightText: 2017-2019 Adriaan de Groot <
[email protected]>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Calamares is Free Software: see the License-Identifier above.
+ *
+ */
+#include "StageChoosePage.h"
+#include "Config.h"
+#include "ui_StageChoosePage.h"
+
+#include <QComboBox>
+#include <QLabel>
+#include <QTimer>
+#include <QPushButton>
+
+StageChoosePage::StageChoosePage(Config* config, QWidget* parent)
+ : QWidget(parent)
+ , ui(new Ui::StageChoosePage)
+ , m_config(config)
+{
+ ui->setupUi(this);
+
+ connect(ui->architectureComboBox, QOverload<int>::of(&QComboBox::activated),
+ this, &StageChoosePage::onArchitectureChanged);
+ connect(ui->variantComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
+ this, &StageChoosePage::onVariantChanged);
+
+ connect(ui->mirrorLineEdit, &QLineEdit::editingFinished, this, &StageChoosePage::onMirrorChanged);
+ connect(ui->restartFetcherButton, &QPushButton::clicked, this, &StageChoosePage::onRestartFetcherClicked);
+
+ if(m_config){
+ connect(m_config, &Config::fetchStatusChanged,this,&StageChoosePage::setFetcherStatus);
+ connect(m_config, &Config::fetchError,this,[this](const QString& error){setFetcherStatus("Error" + error);showRestartFetcherButton(true);});
+ connect(m_config, &Config::variantsReady, this, &StageChoosePage::whenVariantsReady);
+ connect(m_config, &Config::tarballReady, this, [this](const QString&){updateSelectedTarballLabel();});
+ }
+
+ setFetcherStatus("Idle");
+ updateSelectedTarballLabel();
+ showRestartFetcherButton(false);
+
+ populateArchs();
+}
+
+void StageChoosePage::onMirrorChanged()
+{
+ if(!m_config) return;
+ QString mirror = ui->mirrorLineEdit->text().trimmed();
+ m_config->setMirrorBase(mirror);
+}
+
+void StageChoosePage::setFetcherStatus(const QString& status)
+{
+ ui->fetcherStatusLabel->setText("Status: " + status);
+}
+
+void StageChoosePage::showRestartFetcherButton(bool visible)
+{
+ ui->restartFetcherButton->setVisible(false);
+ // To implement
+}
+
+void StageChoosePage::onRestartFetcherClicked(){
+ // Logic here
+ setFetcherStatus("Restarting...");
+ showRestartFetcherButton(false);
+}
+
+void StageChoosePage::populateArchs()
+{
+ if (!m_config)
+ return;
+
+ const auto archs = m_config->availableArchitecturesInfo();
+ ui->architectureComboBox->clear();
+ for(const auto& arch : archs){
+ ui->architectureComboBox->addItem(arch.description,arch.name);
+ }
+ ui->architectureComboBox->setCurrentIndex(-1);
+}
+
+void StageChoosePage::onArchitectureChanged(int index)
+{
+ if (!m_config)
+ return;
+
+ const QString archKey = ui->architectureComboBox->itemData(index).toString();
+ ui->variantComboBox->clear();
+
+ m_config->availableStagesFor(archKey);
+
+ if(archKey == "livecd"){
+ ui->variantComboBox->setVisible(false);
+ ui->variantLabel->setVisible(false);
+
+ // setFetcherStatus("LiveCD mode");
+ // m_config->updateTarball("livecd");
+ showRestartFetcherButton(false);
+ return;
+ }
+ else{
+ ui->variantComboBox->setVisible(true);
+ ui->variantLabel->setVisible(true);
+ }
+}
+
+void StageChoosePage::onVariantChanged(int index)
+{
+ if (!m_config)
+ return;
+
+ const QString variantKey = ui->variantComboBox->itemData(index).toString();
+ m_config->selectVariant(variantKey);
+}
+
+void StageChoosePage::whenVariantsReady(const QStringList &stages)
+{
+ ui->variantComboBox->clear();
+
+ for(const QString& stage : stages){
+ ui->variantComboBox->addItem(stage, stage);
+ }
+
+ if(!stages.isEmpty()){
+ ui->variantComboBox->setCurrentIndex(0);
+ onVariantChanged(0);
+ }
+}
+
+void StageChoosePage::updateSelectedTarballLabel()
+{
+ if (!m_config)
+ return;
+
+ ui->selectedTarballLabel->setText("Selected: " + m_config->selectedStage3());
+}
+
+StageChoosePage::~StageChoosePage()
+{
+ delete ui;
+}
diff --git a/src/modules/stagechoose/StageChoosePage.h b/src/modules/stagechoose/StageChoosePage.h
new file mode 100644
index 0000000000..65e6633184
--- /dev/null
+++ b/src/modules/stagechoose/StageChoosePage.h
@@ -0,0 +1,51 @@
+/* === This file is part of Calamares - <
https://calamares.io> ===
+ *
+ * SPDX-FileCopyrightText: 2014 Teo Mrnjavac <
[email protected]>
+ * SPDX-FileCopyrightText: 2019 Adriaan de Groot <
[email protected]>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Calamares is Free Software: see the License-Identifier above.
+ *
+ */
+
+#ifndef STAGECHOOSEPAGE_H
+#define STAGECHOOSEPAGE_H
+
+#include <QWidget>
+
+class QComboBox;
+class QLabel;
+class Config;
+
+namespace Ui {
+class StageChoosePage;
+}
+
+class StageChoosePage : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit StageChoosePage( Config* config, QWidget* parent = nullptr);
+ ~StageChoosePage() override;
+
+ void populateArchs();
+ void setFetcherStatus(const QString& status);
+ void showRestartFetcherButton(bool visible);
+ void onRestartFetcherClicked();
+ void whenVariantsReady(const QStringList &stages);
+
+ void onMirrorChanged();
+
+private slots:
+ void onArchitectureChanged(int index);
+ void onVariantChanged(int index);
+ void updateSelectedTarballLabel();
+
+private:
+ Ui::StageChoosePage* ui;
+ Config* m_config;
+};
+
+#endif // STAGECHOOSEPAGE_H
+
diff --git a/src/modules/stagechoose/StageChoosePage.ui b/src/modules/stagechoose/StageChoosePage.ui
new file mode 100644
index 0000000000..f78482abd0
--- /dev/null
+++ b/src/modules/stagechoose/StageChoosePage.ui
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>StageChoosePage</class>
+ <widget class="QWidget" name="StageChoosePage">
+ <layout class="QVBoxLayout" name="outerVerticalLayout">
+
+ <item>
+ <spacer name="verticalSpacerTop">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+
+ <item>
+ <layout class="QHBoxLayout" name="horizontalCenteringLayout">
+ <item>
+ <spacer name="horizontalSpacerLeft">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ </spacer>
+ </item>
+
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="mirrorInfoLabel">
+ <property name="text">
+ <string>If you leave mirror link blank, it will choose the default option.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+
+ <item>
+ <layout class="QHBoxLayout" name="mirrorLayout">
+ <item>
+ <widget class="QLabel" name="mirrorLabel">
+ <property name="text">
+ <string>Mirror Link:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="mirrorLineEdit">
+ <property name="placeholderText">
+ <string>
https://distfiles.gentoo.org/releases/</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+
+ <item>
+ <layout class="QHBoxLayout" name="topLabelsLayout">
+ <item>
+ <widget class="QLabel" name="archLabel">
+ <property name="text">
+ <string>Select Architecture:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="variantLabel">
+ <property name="text">
+ <string>Select Stage3 Option:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+
+ <item>
+ <layout class="QHBoxLayout" name="comboBoxesLayout">
+ <item>
+ <widget class="QComboBox" name="architectureComboBox">
+ <property name="minimumSize">
+ <size><width>200</width><height>25</height></size>
+ </property>
+ <property name="maximumSize">
+ <size><width>200</width><height>25</height></size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="variantComboBox">
+ <property name="minimumSize">
+ <size><width>200</width><height>25</height></size>
+ </property>
+ <property name="maximumSize">
+ <size><width>200</width><height>25</height></size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+
+ <item>
+ <widget class="QLabel" name="selectedTarballLabel">
+ <property name="text">
+ <string>Selected: </string>
+ </property>
+ </widget>
+ </item>
+
+ <item>
+ <widget class="QLabel" name="fetcherStatusLabel">
+ <property name="text">
+ <string>Status: Idle</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+
+ <item>
+ <widget class="QPushButton" name="restartFetcherButton">
+ <property name="text">
+ <string>Restart Fetcher</string>
+ </property>
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+
+ <item>
+ <spacer name="horizontalSpacerRight">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+
+ <item>
+ <spacer name="verticalSpacerBottom">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/modules/stagechoose/StageChooseViewStep.cpp b/src/modules/stagechoose/StageChooseViewStep.cpp
new file mode 100644
index 0000000000..24857f07f0
--- /dev/null
+++ b/src/modules/stagechoose/StageChooseViewStep.cpp
@@ -0,0 +1,80 @@
+/* === This file is part of Calamares - <
https://calamares.io> ===
+ *
+ * SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <
[email protected]>
+ * SPDX-FileCopyrightText: 2018 Adriaan de Groot <
[email protected]>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Calamares is Free Software: see the License-Identifier above.
+ *
+ */
+
+#include "StageChooseViewStep.h"
+
+#include "Config.h"
+#include "StageChoosePage.h"
+#include "SetStage3Job.h"
+
+#include "utils/Logger.h"
+
+CALAMARES_PLUGIN_FACTORY_DEFINITION(StageChooseViewStepFactory, registerPlugin<StageChooseViewStep>();)
+
+StageChooseViewStep::StageChooseViewStep(QObject* parent)
+ : Calamares::ViewStep(parent)
+ , m_config(new Config(this))
+ , m_widget(new StageChoosePage(m_config))
+{
+ connect(m_config,&Config::validityChanged,this,[this](bool valid){emit nextStatusChanged(valid);});
+}
+
+StageChooseViewStep::~StageChooseViewStep()
+{
+ if ( m_widget && m_widget->parent() == nullptr )
+ {
+ m_widget->deleteLater();
+ }
+}
+
+QString StageChooseViewStep::prettyName() const
+{
+ return tr("Select Stage");
+}
+
+QWidget* StageChooseViewStep::widget()
+{
+ return m_widget;
+}
+
+bool StageChooseViewStep::isNextEnabled() const
+{
+ return m_config->isValid();
+}
+
+bool StageChooseViewStep::isBackEnabled() const
+{
+ return true;
+}
+
+bool StageChooseViewStep::isAtBeginning() const
+{
+ return true;
+}
+
+bool StageChooseViewStep::isAtEnd() const
+{
+ return true;
+}
+
+void StageChooseViewStep::onLeave()
+{
+ m_config->updateGlobalStorage();
+}
+
+Calamares::JobList StageChooseViewStep::jobs() const
+{
+ Calamares::JobList list;
+ if (m_config->isValid())
+ {
+ list.append(QSharedPointer<SetStage3Job>::create(m_config->selectedStage3()));
+ }
+ return list;
+}
diff --git a/src/modules/stagechoose/StageChooseViewStep.h b/src/modules/stagechoose/StageChooseViewStep.h
new file mode 100644
index 0000000000..d6efed30f4
--- /dev/null
+++ b/src/modules/stagechoose/StageChooseViewStep.h
@@ -0,0 +1,53 @@
+/* === This file is part of Calamares - <
https://calamares.io> ===
+ *
+ * SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac <
[email protected]>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Calamares is Free Software: see the License-Identifier above.
+ *
+ */
+
+#ifndef STAGECHOOSEVIEWSTEP_H
+#define STAGECHOOSEVIEWSTEP_H
+
+#include <QObject>
+#include <QWidget>
+#include <QString>
+
+#include "DllMacro.h"
+#include "utils/PluginFactory.h"
+#include "viewpages/ViewStep.h"
+
+class StageChoosePage;
+class Config;
+
+class PLUGINDLLEXPORT StageChooseViewStep : public Calamares::ViewStep
+{
+ Q_OBJECT
+
+public:
+ explicit StageChooseViewStep(QObject* parent = nullptr);
+ ~StageChooseViewStep() override;
+
+ QString prettyName() const override;
+
+ QWidget* widget() override;
+
+ bool isNextEnabled() const override;
+ bool isBackEnabled() const override;
+ bool isAtBeginning() const override;
+ bool isAtEnd() const override;
+
+ Calamares::JobList jobs() const override;
+
+ void onLeave() override;
+
+private:
+ Config* m_config;
+ StageChoosePage* m_widget;
+};
+
+CALAMARES_PLUGIN_FACTORY_DECLARATION( StageChooseViewStepFactory )
+
+#endif // STAGECHOOSEVIEWSTEP_H
+
diff --git a/src/modules/stagechoose/StageFetcher.cpp b/src/modules/stagechoose/StageFetcher.cpp
new file mode 100644
index 0000000000..287ab59023
--- /dev/null
+++ b/src/modules/stagechoose/StageFetcher.cpp
@@ -0,0 +1,152 @@
+#include "StageFetcher.h"
+
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QEventLoop>
+#include <QRegularExpression>
+#include <QRegularExpressionMatch>
+#include <QRegularExpressionMatchIterator>
+#include <QStringList>
+
+StageFetcher :: StageFetcher(QObject* parent):QObject(parent)
+{
+}
+
+QString StageFetcher::extractvariantBase(const QString& variant){
+ if(variant.startsWith("current-"))
+ return variant.mid(8);
+ return variant;
+}
+
+void StageFetcher::setMirrorBase(const QString& mirror)
+{
+ QString base = mirror.trimmed();
+ while(base.endsWith('/')) base.chop(1);
+
+ if(base.isEmpty())
+ base = QStringLiteral("
http://distfiles.gentoo.org/releases");
+
+ if(!base.endsWith("/releases"))
+ base += "/releases";
+
+ m_mirrorBase = base;
+}
+
+void StageFetcher::cancelOngoingRequest()
+{
+ if(m_currentReply){
+ disconnect(m_currentReply,nullptr,this,nullptr);
+ if(m_currentReply->isRunning())
+ m_currentReply->abort();
+ m_currentReply->deleteLater();
+ m_currentReply = nullptr;
+ }
+}
+
+void StageFetcher::fetchVariants(const QString& arch)
+{
+ cancelOngoingRequest();
+ emit fetchStatusChanged("Fetching variants for " + arch + "...");
+
+ QString urlStr = QString("%1/%2/autobuilds/").arg(m_mirrorBase, arch);
+ QUrl url(urlStr);
+ QNetworkRequest request(url);
+
+ QNetworkReply* reply = m_nam.get(request);
+ m_currentReply = reply;
+ connect(reply, &QNetworkReply::finished, this,[this, reply](){onVariantsReplyFinished(reply);});
+}
+
+void StageFetcher::onVariantsReplyFinished(QNetworkReply* reply)
+{
+ if(!reply)
+ return;
+
+ if(reply != m_currentReply){
+ reply->deleteLater();
+ return;
+ }
+
+ QStringList variants;
+ if(reply->error() != QNetworkReply::NoError){
+ emit fetchError(reply->errorString());
+ reply->deleteLater();
+ if(m_currentReply == reply) m_currentReply = nullptr;
+ return;
+ }
+
+ QString html = reply->readAll();
+ if(html.isEmpty())
+ emit variantsFetched(variants);
+
+ QRegularExpression re(R"((current-stage3-[^"/]+)[/])");
+ QRegularExpressionMatchIterator iterator = re.globalMatch(html);
+
+ QStringList seen;
+ while(iterator.hasNext()){
+ QRegularExpressionMatch match = iterator.next();
+ QString variant = match.captured(1);
+ if(!seen.contains(variant)){
+ variants.append(variant);
+ seen.append(variant);
+ }
+ }
+
+ emit variantsFetched(variants);
+ emit fetchStatusChanged("Idle");
+ reply->deleteLater();
+ if(reply == m_currentReply) m_currentReply = nullptr;
+}
+
+void StageFetcher::fetchLatestTarball(const QString& arch, const QString& variant)
+{
+ cancelOngoingRequest();
+ emit fetchStatusChanged("Fetching Tarball for "+ variant +"...");
+ const QString baseUrl = QString("%1/%2/autobuilds/%3/").arg(m_mirrorBase, arch, variant);
+ QUrl url(baseUrl);
+ QNetworkRequest request(url);
+
+ QNetworkReply* reply = m_nam.get(request);
+ m_currentReply = reply;
+ connect(reply, &QNetworkReply::finished, this, [this, reply, variant](){onTarballReplyFinished(reply, variant);});
+}
+
+void StageFetcher::onTarballReplyFinished(QNetworkReply* reply, const QString& variant)
+{
+ if(!reply)
+ return;
+
+ if(reply != m_currentReply){
+ reply->deleteLater();
+ return;
+ }
+
+ QString latest;
+ if(reply->error() != QNetworkReply::NoError){
+ emit fetchError(reply->errorString());
+ reply->deleteLater();
+ if(m_currentReply == reply) m_currentReply = nullptr;
+ return;
+ }
+
+ QString html = reply->readAll();
+ if(html.isEmpty())
+ emit tarballFetched(latest);
+
+ QRegularExpression re(QString("(%1-[\\dTZ]+\\.tar\\.xz)").arg(StageFetcher::extractvariantBase(variant)));
+ QRegularExpressionMatchIterator iterator = re.globalMatch(html);
+
+ while(iterator.hasNext()){
+ QRegularExpressionMatch match = iterator.next();
+ QString filename = match.captured(1);
+ if(filename > latest){
+ latest = filename;
+ }
+ }
+
+ emit tarballFetched(latest);
+ emit fetchStatusChanged("Idle");
+ reply->deleteLater();
+ if(reply == m_currentReply) m_currentReply = nullptr;
+}
\ No newline at end of file
diff --git a/src/modules/stagechoose/StageFetcher.h b/src/modules/stagechoose/StageFetcher.h
new file mode 100644
index 0000000000..8c97f29b55
--- /dev/null
+++ b/src/modules/stagechoose/StageFetcher.h
@@ -0,0 +1,42 @@
+#ifndef STAGEFETCHER_H
+#define STAGEFETCHER_H
+
+#include <QNetworkAccessManager>
+#include <QObject>
+#include <QNetworkReply>
+#include <QPointer>
+#include <QString>
+#include <QStringList>
+#include <QUrl>
+
+class StageFetcher : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit StageFetcher(QObject* parent =nullptr);
+
+ void fetchVariants(const QString& arch);
+ QString extractvariantBase(const QString& varaint);
+ void fetchLatestTarball(const QString& arch, const QString& variant);
+
+ void setMirrorBase(const QString& mirror);
+ void cancelOngoingRequest();
+
+signals:
+ void fetchStatusChanged(const QString& status);
+ void fetchError(const QString& error);
+ void variantsFetched(const QStringList& variants);
+ void tarballFetched(const QString& tarballs);
+
+private slots:
+ void onVariantsReplyFinished(QNetworkReply* reply);
+ void onTarballReplyFinished(QNetworkReply* reply, const QString& variant);
+
+private:
+ QString m_mirrorBase {QStringLiteral("
http://distfiles.gentoo.org/releases")};
+ QNetworkAccessManager m_nam;
+ QPointer<QNetworkReply> m_currentReply;
+};
+
+#endif //STAGEFETCHER_H
\ No newline at end of file
diff --git a/src/modules/stagechoose/stagechoose.conf b/src/modules/stagechoose/stagechoose.conf
new file mode 100644
index 0000000000..59602206da
--- /dev/null
+++ b/src/modules/stagechoose/stagechoose.conf
@@ -0,0 +1,7 @@
+---
+type: viewmodule
+interface: qtplugin
+module: stagechoose
+
+viewmodule:
+ weight: 30
diff --git a/src/modules/stagechoose/stagechoose.schema.yaml b/src/modules/stagechoose/stagechoose.schema.yaml
new file mode 100644
index 0000000000..d1e3d9aa05
--- /dev/null
+++ b/src/modules/stagechoose/stagechoose.schema.yaml
@@ -0,0 +1,17 @@
+---
+type: map
+mapping:
+ type:
+ type: str
+ required: true
+ interface:
+ type: str
+ required: true
+ module:
+ type: str
+ required: true
+ viewmodule:
+ type: map
+ mapping:
+ weight:
+ type: int