From 2ec22e9be83dd2fcba7e0670d823d3d6c1e98a0b Mon Sep 17 00:00:00 2001
From: TableFlipper9 <hithack9@gmail.com>
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 <mihaimorovan1@gmail.com>
---
 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 <groot@kde.org>
+ *   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 <groot@kde.org>
+ *   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 <teo@kde.org>
+ *   SPDX-FileCopyrightText: 2015 Anke Boersma <demm@kaosx.us>
+ *   SPDX-FileCopyrightText: 2017-2019 Adriaan de Groot <groot@kde.org>
+ *   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 <teo@kde.org>
+ *   SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
+ *   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 <teo@kde.org>
+ *   SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
+ *   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 <teo@kde.org>
+ *   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
