diff --git a/.github/workflows/nightly-opensuse-qt6.yml b/.github/workflows/nightly-opensuse-qt6.yml deleted file mode 100644 index cd286d5db..000000000 --- a/.github/workflows/nightly-opensuse-qt6.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: nightly-opensuse-qt6 - -on: - schedule: - - cron: "32 2 * * *" - workflow_dispatch: - -env: - BUILDDIR: /build - SRCDIR: ${{ github.workspace }} - CMAKE_ARGS: | - -DKDE_INSTALL_USE_QT_SYS_PATHS=ON - -DCMAKE_BUILD_TYPE=Debug - -DWITH_QT6=ON - -jobs: - build: - runs-on: ubuntu-latest - container: - image: docker://opensuse/tumbleweed - options: --tmpfs /build:rw --user 0:0 - steps: - - name: "prepare git" - shell: bash - run: zypper --non-interactive in git-core jq curl - - name: "prepare source" - uses: calamares/actions/generic-checkout@v5 - - name: "install dependencies" - shell: bash - run: ./ci/deps-opensuse-qt6.sh - - name: "build" - shell: bash - run: ./ci/build.sh diff --git a/.github/workflows/nightly-opensuse.yml b/.github/workflows/nightly-opensuse.yml index 5b742b704..a7c2f83ef 100644 --- a/.github/workflows/nightly-opensuse.yml +++ b/.github/workflows/nightly-opensuse.yml @@ -15,6 +15,7 @@ env: -DBUILD_TESTING=ON -DBUILD_APPSTREAM=ON -DBUILD_APPDATA=ON + -DWITH_QT6=ON jobs: build: @@ -30,7 +31,7 @@ jobs: uses: calamares/actions/generic-checkout@v5 - name: "install dependencies" shell: bash - run: ./ci/deps-opensuse.sh + run: ./ci/deps-opensuse-qt6.sh - name: "build" shell: bash run: ./ci/build.sh diff --git a/CHANGES-3.3 b/CHANGES-3.3 index 5312c1e82..9a0d0107e 100644 --- a/CHANGES-3.3 +++ b/CHANGES-3.3 @@ -7,6 +7,33 @@ contributors are listed. Note that Calamares does not have a historical changelog -- this log starts with version 3.3.0. See CHANGES-3.2 for the history of the 3.2 series (2018-05 - 2022-08). +# 3.3.6 (2024-04-16) + +This release contains contributions from (alphabetically by first name): + - Adriaan de Groot + - Anke Boersma + - Eugene Sam + - Evan James + - Harald Sitter + - Mike Stemle + - Peter Jung + - Simon Quigley + +## Core ## + - Various Qt6-related fixes. + - Calamares now prevents sleep and suspend while the installation is + running, so that unattended installs do not accidentally fall asleep. + +## Modules ## + - *bootloader* Adds "splash" to kernel parameters if plymouth is present. + (thanks Eugene) + - *locale* Now picks the correct timezone for Dubai, Muscat, Tehran. + - *plymouthcfg* Use plymouth-set-default-theme to avoid issues with + configuration. (thanks Peter) + - *users* module now supports enrolling in Active Directory, if enabled. + (thanks Simon) + + # 3.3.5 (2024-03-03) This release contains contributions from (alphabetically by first name): diff --git a/CMakeLists.txt b/CMakeLists.txt index 7951b8e8f..99b8d4e41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,7 +47,7 @@ cmake_minimum_required(VERSION 3.16 FATAL_ERROR) -set(CALAMARES_VERSION 3.3.5) +set(CALAMARES_VERSION 3.3.6) set(CALAMARES_RELEASE_MODE ON) # Set to ON during a release if(CMAKE_SCRIPT_MODE_FILE) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 36199a5ca..a73f90589 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -154,10 +154,15 @@ dependencies for the image (in this example, for openSUSE and Qt6). - `./ci/deps-opensuse-qt6.sh` Then run CMake (add any CMake options you like at the end) and ninja. -There is a script `ci/build.sh` that does this, too (without options). - `cmake -S /src -B /build -G Ninja` - `ninja -C /build` +There is a script `ci/build.sh` that does the CMake an ninja steps. +- If you set `CMAKE_ARGS` in the environment those extra CMake options are used. +- If you add an argument to the script command which names a workflow + (e.g. "nightly-opensuse-qt6") then `CMAKE_ARGS` are extracted from that + workflow and used for the build. + ### Running in Docker To run Calamares inside the container, or e.g. `loadmodule` to test diff --git a/ci/build.sh b/ci/build.sh index 60b3bc7ca..56ca770f1 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -5,6 +5,41 @@ # - BUILDDIR (e.g. /build) # - CMAKE_ARGS (e.g. "-DWITH_QT6=ON -DCMAKE_BUILD_TYPE=Debug") # +# If SRCDIR is not set, it is assumed to be the directory above +# wherever this script is being run from (this script is in ci/). +# +# If BUILDDIR is not set, and /build exists (e.g. in the recommended +# Docker setup) then /build is used. +# +# If CMAKE_ARGS is not set, but the script is given an argument +# that exists as a workflow (e.g. "nightly-opensuse-qt6" or +# "nightly-debian.yml") and yq is installed, then the CMAKE_ARGS +# are extracted from that workflow file. +# +# Summary, pick one: +# - set environment variables, run "build.sh" +# - set no variables, run "build.sh " + +if test -z "$SRCDIR" ; then + _d=$(dirname "$0" ) + _d=$(dirname "$_d" ) + test -f "$_d/CMakeLists.txt" && SRCDIR="$_d" +fi +if test -z "$BUILDDIR" ; then + test -d "/build" && BUILDDIR=/build +fi +if test -z "$CMAKE_ARGS" -a -n "$1" ; then + _d="$SRCDIR/.github/workflows/$1" + test -f "$_d" || _d="$SRCDIR/.github/workflows/$1.yml" + test -f "$_d" || { echo "! No workflow $1" ; exit 1 ; } + + if test -x "$(which yq)" ; then + CMAKE_ARGS=$(yq ".env.CMAKE_ARGS" "$_d") + else + CMAKE_ARGS=$(python3 -c 'import yaml ; f=open("'$_d'","r"); print(yaml.safe_load(f)["env"]["CMAKE_ARGS"]);') + fi + +fi # Sanity check test -n "$BUILDDIR" || { echo "! \$BUILDDIR not set" ; exit 1 ; } diff --git a/ci/deps-fedora-qt6-boost.sh b/ci/deps-fedora-qt6-boost.sh index 01e3e3449..7026927a7 100755 --- a/ci/deps-fedora-qt6-boost.sh +++ b/ci/deps-fedora-qt6-boost.sh @@ -4,7 +4,7 @@ # yum install -y bison flex git make cmake gcc-c++ ninja-build -yum install -y yaml-cpp-devel libpwquality-devel parted-devel python-devel gettext gettext-devel +yum install -y yaml-cpp-devel libpwquality-devel parted-devel python-devel gettext gettext-devel python3-pyyaml yum install -y libicu-devel libatasmart-devel yum install -y boost-devel # Qt6/KF6 dependencies diff --git a/ci/deps-fedora-qt6.sh b/ci/deps-fedora-qt6.sh index 2d2e83ce5..9f91ada18 100755 --- a/ci/deps-fedora-qt6.sh +++ b/ci/deps-fedora-qt6.sh @@ -4,7 +4,7 @@ # yum install -y bison flex git make cmake gcc-c++ ninja-build -yum install -y yaml-cpp-devel libpwquality-devel parted-devel python-devel gettext gettext-devel +yum install -y yaml-cpp-devel libpwquality-devel parted-devel python-devel gettext gettext-devel python3-pyyaml yum install -y libicu-devel libatasmart-devel # Qt6/KF6 dependencies yum install -y qt6-qtbase-devel qt6-linguist qt6-qtbase-private-devel qt6-qtdeclarative-devel qt6-qtsvg-devel qt6-qttools-devel diff --git a/ci/deps-opensuse-qt6.sh b/ci/deps-opensuse-qt6.sh index c1138c1c6..1c7f0a3f0 100755 --- a/ci/deps-opensuse-qt6.sh +++ b/ci/deps-opensuse-qt6.sh @@ -8,7 +8,7 @@ zypper --non-interactive addrepo -f -G https://download.opensuse.org/repositorie zypper --non-interactive refresh zypper --non-interactive up -zypper --non-interactive in git-core jq curl ninja +zypper --non-interactive in git-core jq yq curl ninja # From deploycala.py zypper --non-interactive in bison flex git make cmake gcc-c++ zypper --non-interactive in yaml-cpp-devel libpwquality-devel parted-devel python3-devel @@ -18,5 +18,5 @@ zypper --non-interactive in kf6-extra-cmake-modules zypper --non-interactive in "qt6-declarative-devel" "cmake(Qt6Concurrent)" "cmake(Qt6Gui)" "cmake(Qt6Network)" "cmake(Qt6Svg)" "cmake(Qt6Linguist)" zypper --non-interactive in "cmake(KF6CoreAddons)" "cmake(KF6DBusAddons)" "cmake(KF6Crash)" zypper --non-interactive in "cmake(KF6Parts)" # Also installs KF5 things -zypper --non-interactive in "cmake(PolkitQt6-1)" +zypper --non-interactive in "cmake(PolkitQt6-1)" appstream-qt6-devel true diff --git a/src/calamares/testmain.cpp b/src/calamares/testmain.cpp index 203c97936..ad40aadfc 100644 --- a/src/calamares/testmain.cpp +++ b/src/calamares/testmain.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include @@ -455,11 +456,11 @@ libcalamares.utils.debug('pre-script for testing purposes injected') int main( int argc, char* argv[] ) { - QCoreApplication* aw = createApplication( argc, argv ); + QCoreApplication* application = createApplication( argc, argv ); Logger::setupLogLevel( Logger::LOGVERBOSE ); - ModuleConfig module = handle_args( *aw ); + ModuleConfig module = handle_args( *application ); if ( module.moduleName().isEmpty() ) { return 1; @@ -469,7 +470,7 @@ main( int argc, char* argv[] ) std::unique_ptr< Calamares::JobQueue > jobqueue_p( new Calamares::JobQueue( nullptr ) ); std::unique_ptr< Calamares::System > system_p( new Calamares::System( settings_p->doChroot() ) ); - QMainWindow* mw = nullptr; + QMainWindow* mainWindow = nullptr; auto* gs = jobqueue_p->globalStorage(); if ( !module.globalConfigFile().isEmpty() ) @@ -513,21 +514,21 @@ main( int argc, char* argv[] ) // tries to create the widget **which won't be used anyway**. // // To avoid that crash, re-create the QApplication, now with GUI - if ( !qobject_cast< QApplication* >( aw ) ) + if ( !qobject_cast< QApplication* >( application ) ) { auto* replace_app = new QApplication( argc, argv ); replace_app->setQuitOnLastWindowClosed( true ); - aw = replace_app; + application = replace_app; } - mw = module.m_ui ? new QMainWindow() : nullptr; - if ( mw ) + mainWindow = module.m_ui ? new QMainWindow() : nullptr; + if ( mainWindow ) { - mw->installEventFilter( Calamares::Retranslator::instance() ); + mainWindow->installEventFilter( Calamares::Retranslator::instance() ); } (void)new Calamares::Branding( module.m_branding ); auto* modulemanager = new Calamares::ModuleManager( QStringList(), nullptr ); - (void)Calamares::ViewManager::instance( mw ); + (void)Calamares::ViewManager::instance( mainWindow ); modulemanager->addModule( m ); } @@ -542,16 +543,16 @@ main( int argc, char* argv[] ) return 1; } - if ( mw ) + if ( mainWindow ) { auto* vm = Calamares::ViewManager::instance(); vm->onInitComplete(); QWidget* w = vm->currentStep()->widget(); - w->setParent( mw ); - mw->setCentralWidget( w ); + w->setParent( mainWindow ); + mainWindow->setCentralWidget( w ); w->show(); - mw->show(); - return aw->exec(); + mainWindow->show(); + return application->exec(); } using TR = Logger::DebugRow< const char*, const QString >; @@ -559,30 +560,10 @@ main( int argc, char* argv[] ) cDebug() << Logger::SubEntry << "Module metadata" << TR( "name", m->name() ) << TR( "type", m->typeString() ) << TR( "interface", m->interfaceString() ); - Calamares::JobList jobList = m->jobs(); - unsigned int failure_count = 0; - unsigned int count = 1; - for ( const auto& p : jobList ) - { - // This doesn't get a SubEntry because the jobs may log a bunch of - // things; print the function-header to make clear that we're back in main. - cDebug() << "Job #" << count << "name" << p->prettyName(); - Calamares::JobResult r = p->exec(); - if ( !r ) - { - cError() << "Job #" << count << "failed" << TR( "summary", r.message() ) << TR( "details", r.details() ); - if ( r.errorCode() > 0 ) - { - ++failure_count; - } - } - ++count; - } + Calamares::JobQueue::instance()->enqueue(100, m->jobs()); - if ( aw ) - { - delete aw; - } + QObject::connect(Calamares::JobQueue::instance(), &Calamares::JobQueue::finished, [application]() { QTimer::singleShot(std::chrono::seconds(3), application, &QApplication::quit); }); + QTimer::singleShot(0, []() { Calamares::JobQueue::instance()->start(); }); - return failure_count ? 1 : 0; + return application->exec(); } diff --git a/src/libcalamares/Job.h b/src/libcalamares/Job.h index 241b2883c..931029a8f 100644 --- a/src/libcalamares/Job.h +++ b/src/libcalamares/Job.h @@ -109,12 +109,23 @@ public: * which of the jobs is "heavy" and which is not. */ virtual int getJobWeight() const; + /** @brief The human-readable name of this job * * This should be a very short statement of what the job does. * For status and state information, see prettyStatusMessage(). + * + * The job's name may be similar to the status message, but this is + * a name, and should not be an active verb phrase. The translation + * should use context @c \@label . + * + * The name of the job is used as a **fallback** when the status + * or descriptions are empty. If a job has no implementation of + * those methods, it is OK to use other contexts, but it may look + * strange in some places in the UI. */ virtual QString prettyName() const = 0; + /** @brief a longer human-readable description of what the job will do * * This **may** be used by view steps to fill in the summary @@ -122,15 +133,23 @@ public: * module does so. * * The default implementation returns an empty string. + * + * The translation should use context @c \@title . */ virtual QString prettyDescription() const; + /** @brief A human-readable status for progress reporting * * This is called from the JobQueue when progress is made, and should * return a not-too-long description of the job's status. This * is made visible in the progress bar of the execution view step. + * + * The job's status should say **what** the job is doing. It should be in + * present active tense. Typically the translation uses tr() context + * @c \@status . See prettyName() for examples. */ virtual QString prettyStatusMessage() const; + virtual JobResult exec() = 0; bool isEmergency() const { return m_emergency; } diff --git a/src/libcalamares/JobQueue.cpp b/src/libcalamares/JobQueue.cpp index 64cb10e88..80e49dce4 100644 --- a/src/libcalamares/JobQueue.cpp +++ b/src/libcalamares/JobQueue.cpp @@ -16,13 +16,174 @@ #include "compat/Mutex.h" #include "utils/Logger.h" +#include +#include +#include +#include +#include #include #include #include +namespace +{ +// This power-management code is largely cribbed from KDE Discover, +// https://invent.kde.org/plasma/discover/-/blob/master/discover/PowerManagementInterface.cpp +// +// Upstream license text says: +// +// SPDX-FileCopyrightText: 2019 (c) Matthieu Gallien +// SPDX-License-Identifier: LGPL-3.0-or-later + + +/** @brief Class to manage sleep / suspend on inactivity + * + * Create an object of this class on the heap. Call inhibitSleep() + * to (try to) stop system sleep / suspend. Call uninhibitSleep() + * when the object is no longer needed. The object self-deletes + * after uninhibitSleep() completes. + */ +class PowerManagementInterface : public QObject +{ + Q_OBJECT +public: + PowerManagementInterface( QObject* parent = nullptr ); + ~PowerManagementInterface() override; + +public Q_SLOTS: + void inhibitSleep(); + void uninhibitSleep(); + +private Q_SLOTS: + void hostSleepInhibitChanged(); + void inhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher ); + void uninhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher ); + +private: + uint m_inhibitSleepCookie = 0; + bool m_inhibitedSleep = false; +}; + +PowerManagementInterface::PowerManagementInterface( QObject* parent ) + : QObject( parent ) +{ + auto sessionBus = QDBusConnection::sessionBus(); + + sessionBus.connect( QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ), + QStringLiteral( "/org/freedesktop/PowerManagement/Inhibit" ), + QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ), + QStringLiteral( "HasInhibitChanged" ), + this, + SLOT( hostSleepInhibitChanged() ) ); +} + +PowerManagementInterface::~PowerManagementInterface() = default; + +void +PowerManagementInterface::hostSleepInhibitChanged() +{ + // We don't actually care +} + +void +PowerManagementInterface::inhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher ) +{ + QDBusPendingReply< uint > reply = *aWatcher; + if ( reply.isError() ) + { + cError() << "Could not inhibit sleep:" << reply.error(); + // m_inhibitedSleep = false; // unchanged + } + else + { + m_inhibitSleepCookie = reply.argumentAt< 0 >(); + m_inhibitedSleep = true; + cDebug() << "Sleep inhibited, cookie" << m_inhibitSleepCookie; + } + aWatcher->deleteLater(); +} + +void +PowerManagementInterface::uninhibitDBusCallFinished( QDBusPendingCallWatcher* aWatcher ) +{ + QDBusPendingReply<> reply = *aWatcher; + if ( reply.isError() ) + { + cError() << "Could not uninhibit sleep:" << reply.error(); + } + else + { + m_inhibitedSleep = false; + m_inhibitSleepCookie = 0; + cDebug() << "Sleep uninhibited."; + } + aWatcher->deleteLater(); + this->deleteLater(); +} + +void +PowerManagementInterface::inhibitSleep() +{ + if ( m_inhibitedSleep ) + { + cDebug() << "Sleep is already inhibited."; + return; + } + + auto sessionBus = QDBusConnection::sessionBus(); + auto inhibitCall = QDBusMessage::createMethodCall( QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ), + QStringLiteral( "/org/freedesktop/PowerManagement/Inhibit" ), + QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ), + QStringLiteral( "Inhibit" ) ); + inhibitCall.setArguments( + { { tr( "Calamares" ) }, { tr( "Installation in progress", "@status" ) } } ); + + auto asyncReply = sessionBus.asyncCall( inhibitCall ); + auto* replyWatcher = new QDBusPendingCallWatcher( asyncReply, this ); + QObject::connect( + replyWatcher, &QDBusPendingCallWatcher::finished, this, &PowerManagementInterface::inhibitDBusCallFinished ); +} + +void +PowerManagementInterface::uninhibitSleep() +{ + if ( !m_inhibitedSleep ) + { + cDebug() << "Sleep was never inhibited."; + this->deleteLater(); + return; + } + + auto sessionBus = QDBusConnection::sessionBus(); + auto uninhibitCall = QDBusMessage::createMethodCall( QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ), + QStringLiteral( "/org/freedesktop/PowerManagement/Inhibit" ), + QStringLiteral( "org.freedesktop.PowerManagement.Inhibit" ), + QStringLiteral( "UnInhibit" ) ); + uninhibitCall.setArguments( { { m_inhibitSleepCookie } } ); + + auto asyncReply = sessionBus.asyncCall( uninhibitCall ); + auto replyWatcher = new QDBusPendingCallWatcher( asyncReply, this ); + QObject::connect( + replyWatcher, &QDBusPendingCallWatcher::finished, this, &PowerManagementInterface::uninhibitDBusCallFinished ); +} + +} // namespace + namespace Calamares { +SleepInhibitor::SleepInhibitor() +{ + // Create a PowerManagementInterface object with intentionally no parent + // so it is not destroyed along with this. Instead, when this + // is destroyed, **start** the uninhibit-sleep call which will (later) + // destroy the PowerManagementInterface object. + auto* p = new PowerManagementInterface( nullptr ); + p->inhibitSleep(); + connect( this, &QObject::destroyed, p, &PowerManagementInterface::uninhibitSleep ); +} + +SleepInhibitor::~SleepInhibitor() = default; struct WeightedJob { @@ -188,6 +349,13 @@ private: // starts the job, or if the job itself reports 0.0) be more // accepting in what gets reported: jobs with no status fall // back to description and name, whichever is non-empty. + // + // Later calls (e.g. when percentage > 0) use the status unchanged. + // It may be empty, but the ExecutionViewStep knows about empty + // status messages and does not update the text in that case. + // + // This means that a Job can implement just prettyName() and get + // a reasonable "status" message which will update only once. if ( percentage == 0.0 && message.isEmpty() ) { message = jobitem.job->prettyDescription(); @@ -267,6 +435,10 @@ JobQueue::start() m_thread->finalize(); m_finished = false; m_thread->start(); + + auto* inhibitor = new PowerManagementInterface( this ); + inhibitor->inhibitSleep(); + connect( this, &JobQueue::finished, inhibitor, &PowerManagementInterface::uninhibitSleep ); } diff --git a/src/libcalamares/JobQueue.h b/src/libcalamares/JobQueue.h index dd27f0a58..593bb06a1 100644 --- a/src/libcalamares/JobQueue.h +++ b/src/libcalamares/JobQueue.h @@ -20,6 +20,15 @@ namespace Calamares class GlobalStorage; class JobThread; +///@brief RAII class to suppress sleep / suspend during its lifetime +class DLLEXPORT SleepInhibitor : public QObject +{ + Q_OBJECT +public: + SleepInhibitor(); + ~SleepInhibitor() override; +}; + class DLLEXPORT JobQueue : public QObject { Q_OBJECT diff --git a/src/libcalamares/utils/moc-warnings.h b/src/libcalamares/utils/moc-warnings.h index 7d54c26b5..05ba34bd6 100644 --- a/src/libcalamares/utils/moc-warnings.h +++ b/src/libcalamares/utils/moc-warnings.h @@ -25,4 +25,8 @@ #pragma clang diagnostic ignored "-Wextra-semi-stmt" #pragma clang diagnostic ignored "-Wredundant-parens" #pragma clang diagnostic ignored "-Wreserved-identifier" + +#if __clang_major__ >= 17 +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" +#endif #endif diff --git a/src/modules/bootloader/main.py b/src/modules/bootloader/main.py index 189902839..0a9e96598 100644 --- a/src/modules/bootloader/main.py +++ b/src/modules/bootloader/main.py @@ -125,23 +125,28 @@ def is_zfs_root(partition): return partition["mountPoint"] == "/" and partition["fs"] == "zfs" +def have_program_in_target(program : str): + """Returns @c True if @p program is in path in the target""" + return libcalamares.utils.target_env_call(["/usr/bin/which", program]) == 0 + + def get_kernel_params(uuid): + # Configured kernel parameters (default "quiet"), if plymouth installed, add splash + # screen parameter and then "rw". kernel_params = libcalamares.job.configuration.get("kernelParams", ["quiet"]) + if have_program_in_target("plymouth"): + kernel_params.append("splash") kernel_params.append("rw") + use_systemd_naming = have_program_in_target("dracut") or (libcalamares.utils.target_env_call(["/usr/bin/grep", "-q", "^HOOKS.*systemd", "/etc/mkinitcpio.conf"]) == 0) + partitions = libcalamares.globalstorage.value("partitions") + + cryptdevice_params = [] swap_uuid = "" swap_outer_mappername = None swap_outer_uuid = None - cryptdevice_params = [] - - has_dracut = libcalamares.utils.target_env_call(["sh", "-c", "which dracut"]) == 0 - uses_systemd_hook = libcalamares.utils.target_env_call(["sh", "-c", - "grep -q \"^HOOKS.*systemd\" /etc/mkinitcpio.conf"]) == 0 - use_systemd_naming = has_dracut or uses_systemd_hook - - # Take over swap settings: # - unencrypted swap partition sets swap_uuid # - encrypted root sets cryptdevice_params diff --git a/src/modules/hostinfo/HostInfoJob.cpp b/src/modules/hostinfo/HostInfoJob.cpp index d09de3ae2..51de1fadc 100644 --- a/src/modules/hostinfo/HostInfoJob.cpp +++ b/src/modules/hostinfo/HostInfoJob.cpp @@ -100,7 +100,7 @@ hostCPU_FreeBSD() #if defined( Q_OS_LINUX ) static QString -hostCPUmatchARM( const QString& s ) +hostCPUmatchARM( const QString& ) { /* The "CPU implementer" line is for ARM CPUs in general. * diff --git a/src/modules/locale/images/timezone_3.0.png b/src/modules/locale/images/timezone_3.0.png index d5c003b4e..5cdf8e04e 100644 Binary files a/src/modules/locale/images/timezone_3.0.png and b/src/modules/locale/images/timezone_3.0.png differ diff --git a/src/modules/locale/images/timezone_3.5.png b/src/modules/locale/images/timezone_3.5.png index f803c691f..7900d2e75 100644 Binary files a/src/modules/locale/images/timezone_3.5.png and b/src/modules/locale/images/timezone_3.5.png differ diff --git a/src/modules/locale/images/timezone_4.0.png b/src/modules/locale/images/timezone_4.0.png index 674ce4e6e..21493ae44 100644 Binary files a/src/modules/locale/images/timezone_4.0.png and b/src/modules/locale/images/timezone_4.0.png differ diff --git a/src/modules/locale/images/timezone_5.0.png b/src/modules/locale/images/timezone_5.0.png index a15aaccc0..e6ddf2d7a 100644 Binary files a/src/modules/locale/images/timezone_5.0.png and b/src/modules/locale/images/timezone_5.0.png differ diff --git a/src/modules/localeq/Map-qt6.qml b/src/modules/localeq/Map-qt6.qml index b485dcadf..7c8473e08 100644 --- a/src/modules/localeq/Map-qt6.qml +++ b/src/modules/localeq/Map-qt6.qml @@ -1,6 +1,6 @@ /* === This file is part of Calamares - === * - * SPDX-FileCopyrightText: 2020 - 2022 Anke Boersma + * SPDX-FileCopyrightText: 2020 - 2024 Anke Boersma * SPDX-License-Identifier: GPL-3.0-or-later * * Calamares is Free Software: see the License-Identifier above. @@ -112,7 +112,7 @@ Column { Plugin { id: mapPlugin - preferred: ["osm", "esri"] // "esri", "here", "itemsoverlay", "mapbox", "mapboxgl", "osm" + name: ["osm"] } Map { @@ -177,6 +177,30 @@ Column { getTzOffline(); } } + + WheelHandler { + id: wheel + acceptedDevices: Qt.platform.pluginName === "cocoa" || Qt.platform.pluginName === "wayland" + ? PointerDevice.Mouse | PointerDevice.TouchPad + : PointerDevice.Mouse + rotationScale: 1/120 + property: "zoomLevel" + } + DragHandler { + id: drag + target: null + onTranslationChanged: (delta) => map.pan(-delta.x, -delta.y) + } + Shortcut { + enabled: map.zoomLevel < map.maximumZoomLevel + sequence: StandardKey.ZoomIn + onActivated: map.zoomLevel = Math.round(map.zoomLevel + 1) + } + Shortcut { + enabled: map.zoomLevel > map.minimumZoomLevel + sequence: StandardKey.ZoomOut + onActivated: map.zoomLevel = Math.round(map.zoomLevel - 1) + } } Column { diff --git a/src/modules/partition/PartitionViewStep.cpp b/src/modules/partition/PartitionViewStep.cpp index 8b7225da3..119bf1baa 100644 --- a/src/modules/partition/PartitionViewStep.cpp +++ b/src/modules/partition/PartitionViewStep.cpp @@ -223,7 +223,7 @@ PartitionViewStep::prettyStatus() const const QList< PartitionCoreModule::SummaryInfo > list = m_core->createSummaryInfo(); cDebug() << "Summary for Partition" << list.length() << choice; - auto joinDiskInfo = [ choice = choice ]( QString& s, const PartitionCoreModule::SummaryInfo& i ) + auto joinDiskInfo = [ choice ]( QString& s, const PartitionCoreModule::SummaryInfo& i ) { return s + diskDescription( 1, i, choice ); }; const QString diskInfoLabel = std::accumulate( list.begin(), list.end(), QString(), joinDiskInfo ); const QString jobsLabel = jobDescriptions( jobs() ).join( QStringLiteral( "
" ) ); @@ -497,11 +497,12 @@ shouldWarnForNotEncryptedBoot( const Config* config, const PartitionCoreModule* Partition* root_p = core->findPartitionByMountPoint( "/" ); Partition* boot_p = core->findPartitionByMountPoint( "/boot" ); - if ( root_p and boot_p ) + if ( root_p && boot_p ) { - if ( ( root_p->fileSystem().type() == FileSystem::Luks && boot_p->fileSystem().type() != FileSystem::Luks ) - || ( root_p->fileSystem().type() == FileSystem::Luks2 - && boot_p->fileSystem().type() != FileSystem::Luks2 ) ) + const auto encryptionMismatch + = [ root_t = root_p->fileSystem().type(), boot_t = boot_p->fileSystem().type() ]( FileSystem::Type t ) + { return root_t == t && boot_t != t; }; + if ( encryptionMismatch( FileSystem::Luks ) || encryptionMismatch( FileSystem::Luks2 ) ) { return true; } diff --git a/src/modules/partition/core/PartUtils.cpp b/src/modules/partition/core/PartUtils.cpp index bc3e6f5a0..e233403b4 100644 --- a/src/modules/partition/core/PartUtils.cpp +++ b/src/modules/partition/core/PartUtils.cpp @@ -581,12 +581,7 @@ efiFilesystemMinimumSize() uefisys_part_sizeB = v > 0 ? v : 0; } // There is a lower limit of what can be configured - if ( uefisys_part_sizeB < efiSpecificationHardMinimumSize ) - { - uefisys_part_sizeB = efiSpecificationHardMinimumSize; - } - return uefisys_part_sizeB; - return efiSpecificationHardMinimumSize; + return std::max( uefisys_part_sizeB, efiSpecificationHardMinimumSize ); } QString diff --git a/src/modules/partition/partition.conf b/src/modules/partition/partition.conf index 0f23323f8..0975179de 100644 --- a/src/modules/partition/partition.conf +++ b/src/modules/partition/partition.conf @@ -193,7 +193,7 @@ initialSwapChoice: none # Default filesystem type, used when a "new" partition is made. # -# When replacing a partition, the new filesystem type will be from the +# When replacing a partition, the new filesystem type will be from the # defaultFileSystemType value. In other cases, e.g. Erase and Alongside, # as well as when using manual partitioning and creating a new # partition, this filesystem type is pre-selected. Note that @@ -248,9 +248,9 @@ defaultFileSystemType: "ext4" # for root that uses 100% of the space and uses the filesystem defined by # defaultFileSystemType. # -# Note: the EFI system partition is prepend automatically to the layout if -# needed; the swap partition is appended to the layout if enabled (small of -# suspend). +# Note: the EFI system partition is prepended automatically to the layout if +# needed; the swap partition is appended to the layout if enabled (selections +# "small" or "suspend" in *userSwapChoices*). # # Otherwise, the partition layout is defined as follow: # diff --git a/src/modules/plymouthcfg/main.py b/src/modules/plymouthcfg/main.py index 5e66fce67..529d3426e 100644 --- a/src/modules/plymouthcfg/main.py +++ b/src/modules/plymouthcfg/main.py @@ -48,9 +48,7 @@ class PlymouthController: def setTheme(self): plymouth_theme = libcalamares.job.configuration["plymouth_theme"] - target_env_call(["sed", "-e", 's|^.*Theme=.*|Theme=' + - plymouth_theme + '|', "-i", - "/etc/plymouth/plymouthd.conf"]) + target_env_call(["plymouth-set-default-theme", plymouth_theme]) def run(self): if detect_plymouth(): diff --git a/src/modules/users/ActiveDirectoryJob.cpp b/src/modules/users/ActiveDirectoryJob.cpp new file mode 100644 index 000000000..deb4c82d7 --- /dev/null +++ b/src/modules/users/ActiveDirectoryJob.cpp @@ -0,0 +1,85 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2024 Simon Quigley + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "ActiveDirectoryJob.h" + +#include "Config.h" + +#include "GlobalStorage.h" +#include "JobQueue.h" +#include "utils/Logger.h" +#include "utils/Permissions.h" +#include "utils/System.h" + +#include +#include +#include +#include +#include +#include + +ActiveDirectoryJob::ActiveDirectoryJob( const QString& adminLogin, + const QString& adminPassword, + const QString& domain, + const QString& ip ) + : Calamares::Job() + , m_adminLogin( adminLogin ) + , m_adminPassword( adminPassword ) + , m_domain( domain ) + , m_ip( ip ) +{ +} + +QString +ActiveDirectoryJob::prettyName() const +{ + return tr( "Enroll system in Active Directory", "@label" ); +} + +QString +ActiveDirectoryJob::prettyStatusMessage() const +{ + return tr( "Enrolling system in Active Directory…", "@status" ); +} + +Calamares::JobResult +ActiveDirectoryJob::exec() +{ + if ( !m_ip.isEmpty() ) + { + const QString hostsFilePath = Calamares::System::instance()->targetPath( QStringLiteral( "/etc/hosts" ) ); + ; + QFile hostsFile( hostsFilePath ); + if ( hostsFile.open( QIODevice::Append | QIODevice::Text ) ) + { + QTextStream out( &hostsFile ); + out << m_ip << " " << m_domain << "\n"; + hostsFile.close(); + } + else + { + return Calamares::JobResult::error( "Failed to open /etc/hosts for writing." ); + } + } + + const QString installPath = Calamares::System::instance()->targetPath( QStringLiteral( "/" ) ); + auto r = Calamares::System::instance()->runCommand( + Calamares::System::RunLocation::RunInHost, + { "realm", "join", m_domain, "-U", m_adminLogin, "--install=" + installPath, "--verbose" }, + QString(), + m_adminPassword, + std::chrono::seconds( 30 ) ); + + + if ( r.getExitCode() == 0 ) + { + return Calamares::JobResult::ok(); + } + else + { + return Calamares::JobResult::error( QString( "Failed to join realm: %1" ).arg( r.getOutput() ) ); + } +} diff --git a/src/modules/users/ActiveDirectoryJob.h b/src/modules/users/ActiveDirectoryJob.h new file mode 100644 index 000000000..77fd74057 --- /dev/null +++ b/src/modules/users/ActiveDirectoryJob.h @@ -0,0 +1,34 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2024 Simon Quigley + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + */ + +#ifndef ACTIVEDIRECTORYJOB_H +#define ACTIVEDIRECTORYJOB_H + +#include "Job.h" + +class ActiveDirectoryJob : public Calamares::Job +{ + Q_OBJECT +public: + ActiveDirectoryJob( const QString& adminLogin, + const QString& adminPassword, + const QString& domain, + const QString& ip ); + QString prettyName() const override; + QString prettyStatusMessage() const override; + Calamares::JobResult exec() override; + +private: + QString m_adminLogin; // Admin credentials to do the enrollment + QString m_adminPassword; + QString m_domain; + QString m_ip; +}; + +#endif /* ACTIVEDIRECTORYJOB_H */ diff --git a/src/modules/users/CMakeLists.txt b/src/modules/users/CMakeLists.txt index 7a8a944b7..25e011c8e 100644 --- a/src/modules/users/CMakeLists.txt +++ b/src/modules/users/CMakeLists.txt @@ -55,6 +55,7 @@ include_directories(${PROJECT_BINARY_DIR}/src/libcalamaresui) set(_users_src # Jobs + ActiveDirectoryJob.cpp CreateUserJob.cpp MiscJobs.cpp SetPasswordJob.cpp diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 1e6db0f33..cd56bc3e2 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -9,6 +9,7 @@ #include "Config.h" +#include "ActiveDirectoryJob.h" #include "CreateUserJob.h" #include "MiscJobs.h" #include "SetHostNameJob.h" @@ -444,8 +445,6 @@ makeHostnameSuggestion( const QString& templateString, const QStringList& fullNa QString hostnameSuggestion = d.expand( templateString ); - // RegExp for valid hostnames; if the suggestion produces a valid name, return it - static const QRegularExpression HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); return hostnameSuggestion.indexOf( HOSTNAME_RX ) != -1 ? hostnameSuggestion : QString(); } @@ -656,6 +655,48 @@ Config::setRootPasswordSecondary( const QString& s ) } } +void +Config::setActiveDirectoryUsed( bool used ) +{ + m_activeDirectoryUsed = used; +} + +bool +Config::getActiveDirectoryEnabled() const +{ + return m_activeDirectory; +} + +bool +Config::getActiveDirectoryUsed() const +{ + return m_activeDirectoryUsed && m_activeDirectory; +} + +void +Config::setActiveDirectoryAdminUsername( const QString& s ) +{ + m_activeDirectoryAdminUsername = s; +} + +void +Config::setActiveDirectoryAdminPassword( const QString& s ) +{ + m_activeDirectoryAdminPassword = s; +} + +void +Config::setActiveDirectoryDomain( const QString& s ) +{ + m_activeDirectoryDomain = s; +} + +void +Config::setActiveDirectoryIP( const QString& s ) +{ + m_activeDirectoryIP = s; +} + QString Config::rootPassword() const { @@ -913,6 +954,9 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) m_sudoStyle = Calamares::getBool( configurationMap, "sudoersConfigureWithGroup", false ) ? SudoStyle::UserAndGroup : SudoStyle::UserOnly; + // Handle Active Directory enablement + m_activeDirectory = Calamares::getBool( configurationMap, "allowActiveDirectory", false ); + // Handle *hostname* key and subkeys and legacy settings { bool ok = false; // Ignored @@ -990,6 +1034,15 @@ Config::createJobs() const jobs.append( Calamares::job_ptr( j ) ); } + if ( getActiveDirectoryUsed() ) + { + j = new ActiveDirectoryJob( m_activeDirectoryAdminUsername, + m_activeDirectoryAdminPassword, + m_activeDirectoryDomain, + m_activeDirectoryIP ); + jobs.append( Calamares::job_ptr( j ) ); + } + j = new SetupGroupsJob( this ); jobs.append( Calamares::job_ptr( j ) ); diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 599fcd6bd..07fa40d1f 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -226,6 +226,10 @@ public: bool permitWeakPasswords() const { return m_permitWeakPasswords; } /// Current setting for "require strong password"? bool requireStrongPasswords() const { return m_requireStrongPasswords; } + /// Is Active Directory enabled in the config file? + bool getActiveDirectoryEnabled() const; + /// Is it both enabled and activated by user choice (checkbox)? + bool getActiveDirectoryUsed() const; const QList< GroupDescription >& defaultGroups() const { return m_defaultGroups; } /** @brief the names of all the groups for the current user @@ -292,6 +296,12 @@ public Q_SLOTS: void setRootPassword( const QString& ); void setRootPasswordSecondary( const QString& ); + void setActiveDirectoryUsed( bool used ); + void setActiveDirectoryAdminUsername( const QString& ); + void setActiveDirectoryAdminPassword( const QString& ); + void setActiveDirectoryDomain( const QString& ); + void setActiveDirectoryIP( const QString& ); + signals: void userShellChanged( const QString& ); void autoLoginGroupChanged( const QString& ); @@ -343,6 +353,13 @@ private: bool m_isReady = false; ///< Used to reduce readyChanged signals + bool m_activeDirectory = false; + bool m_activeDirectoryUsed = false; + QString m_activeDirectoryAdminUsername; + QString m_activeDirectoryAdminPassword; + QString m_activeDirectoryDomain; + QString m_activeDirectoryIP; + HostNameAction m_hostnameAction = HostNameAction::EtcHostname; bool m_writeEtcHosts = false; QString m_hostnameTemplate; diff --git a/src/modules/users/Tests.cpp b/src/modules/users/Tests.cpp index 1f9efc35e..ba2fd2dab 100644 --- a/src/modules/users/Tests.cpp +++ b/src/modules/users/Tests.cpp @@ -340,7 +340,7 @@ UserTests::testPasswordChecks() QCOMPARE( l.length(), 0 ); QVERIFY( !addPasswordCheck( "nonempty", QVariant( false ), l ) ); // legacy option, now ignored QCOMPARE( l.length(), 0 ); - QVERIFY( !addPasswordCheck( "nonempty", QVariant( true ), l ) ); // still ignored + QVERIFY( !addPasswordCheck( "nonempty", QVariant( true ), l ) ); // still ignored QCOMPARE( l.length(), 0 ); } } diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index bac30f350..1ecc0ebef 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -162,6 +162,16 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) config, &Config::requireStrongPasswordsChanged, ui->checkBoxRequireStrongPassword, &QCheckBox::setChecked ); } + // Active Directory is not checked or enabled by default + ui->useADCheckbox->setVisible( m_config->getActiveDirectoryEnabled() ); + onActiveDirectoryToggled( false ); + + connect( ui->useADCheckbox, &QCheckBox::toggled, this, &UsersPage::onActiveDirectoryToggled ); + connect( ui->domainField, &QLineEdit::textChanged, config, &Config::setActiveDirectoryDomain ); + connect( ui->domainAdminField, &QLineEdit::textChanged, config, &Config::setActiveDirectoryAdminUsername ); + connect( ui->domainPasswordField, &QLineEdit::textChanged, config, &Config::setActiveDirectoryAdminPassword ); + connect( ui->ipAddressField, &QLineEdit::textChanged, config, &Config::setActiveDirectoryIP ); + CALAMARES_RETRANSLATE_SLOT( &UsersPage::retranslate ); onReuseUserPasswordChanged( m_config->reuseUserPasswordForRoot() ); @@ -283,3 +293,18 @@ UsersPage::onReuseUserPasswordChanged( const int checked ) ui->textBoxRootPassword->setVisible( visible ); ui->textBoxVerifiedRootPassword->setVisible( visible ); } + +void +UsersPage::onActiveDirectoryToggled( bool checked ) +{ + ui->domainLabel->setVisible( checked ); + ui->domainField->setVisible( checked ); + ui->domainAdminLabel->setVisible( checked ); + ui->domainAdminField->setVisible( checked ); + ui->domainPasswordField->setVisible( checked ); + ui->domainPasswordLabel->setVisible( checked ); + ui->ipAddressField->setVisible( checked ); + ui->ipAddressLabel->setVisible( checked ); + + m_config->setActiveDirectoryUsed( checked ); +} diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index 2d48f1fa3..379176ab2 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -44,6 +44,8 @@ protected slots: void reportUserPasswordStatus( int, const QString& ); void reportRootPasswordStatus( int, const QString& ); + void onActiveDirectoryToggled( bool checked ); + private: void retranslate(); diff --git a/src/modules/users/page_usersetup.ui b/src/modules/users/page_usersetup.ui index c21907415..6e6e5423e 100644 --- a/src/modules/users/page_usersetup.ui +++ b/src/modules/users/page_usersetup.ui @@ -603,6 +603,93 @@ SPDX-License-Identifier: GPL-3.0-or-later + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 6 + + + + + + + + + + Use Active Directory + + + + + + + + + + + Domain: + + + + + + + + + + + + + + Domain Administrator: + + + + + + + + + + Password: + + + + + + + QLineEdit::Password + + + + + + + + + + + IP Address (optional): + + + + + + + + + + + + diff --git a/src/modules/users/users.conf b/src/modules/users/users.conf index 669cac038..e6910bcd2 100644 --- a/src/modules/users/users.conf +++ b/src/modules/users/users.conf @@ -265,6 +265,12 @@ hostname: template: "derp-${cpu}" forbidden_names: [ localhost ] +# Enable Active Directory enrollment support (opt-in) +# +# This uses realmd to enroll the machine in an Active Directory server +# It requires realmd as a runtime dependency of Calamares, if enabled +allowActiveDirectory: false + presets: fullName: # value: "OEM User" diff --git a/src/modules/users/users.schema.yaml b/src/modules/users/users.schema.yaml index a67504321..c751a5226 100644 --- a/src/modules/users/users.schema.yaml +++ b/src/modules/users/users.schema.yaml @@ -52,6 +52,7 @@ properties: writeHostsFile: { type: boolean, default: true } template: { type: string, default: "${first}-${product}" } forbidden_names: { type: array, items: { type: string } } + allowActiveDirectory: { type: boolean, default: false } # Presets # diff --git a/src/qml/calamares-qt6/slideshow/Presentation.qml b/src/qml/calamares-qt6/slideshow/Presentation.qml index aab7007e6..1eed2e842 100644 --- a/src/qml/calamares-qt6/slideshow/Presentation.qml +++ b/src/qml/calamares-qt6/slideshow/Presentation.qml @@ -196,6 +196,8 @@ Item { Text { id: notesText + property real padding: 16; + x: padding y: padding width: parent.width - 2 * padding