diff options
33 files changed, 1857 insertions, 4 deletions
diff --git a/.DS_Store b/.DS_Store Binary files differdeleted file mode 100644 index 23f73e2..0000000 --- a/.DS_Store +++ /dev/null @@ -8,3 +8,8 @@ src/ *.pkg.tar.zst.sig *.pkg.tar.xz *.pkg.tar.xz.sig + +# Python / macOS caches +__pycache__/ +*.py[cod] +.DS_Store diff --git a/raveos-calamares-module/CMakeLists.txt b/raveos-calamares-module/CMakeLists.txt index 1a56796..bcec971 100644 --- a/raveos-calamares-module/CMakeLists.txt +++ b/raveos-calamares-module/CMakeLists.txt @@ -7,3 +7,4 @@ find_package(Calamares REQUIRED) add_subdirectory(wifi) add_subdirectory(welcome) add_subdirectory(desktopselect) +add_subdirectory(raveusers) diff --git a/raveos-calamares-module/PKGBUILD b/raveos-calamares-module/PKGBUILD index 9006d8e..050a1bd 100644 --- a/raveos-calamares-module/PKGBUILD +++ b/raveos-calamares-module/PKGBUILD @@ -2,13 +2,13 @@ pkgname=raveos-calamares-module pkgver=2.0.0 pkgrel=28 -pkgdesc="RaveOS Calamares modules: Wi-Fi, Welcome, Desktop Selector" +pkgdesc="RaveOS Calamares modules: Wi-Fi, Welcome, Desktop Selector, Users" arch=('x86_64') options=('!debug') url="https://git.rp1.hu/RaveOS" license=('GPL') -makedepends=('cmake' 'qt6-tools' 'kcoreaddons' 'calamares') +makedepends=('cmake' 'qt6-tools' 'kcoreaddons' 'calamares' 'libxcrypt') depends=('qt6-base' 'qt6-declarative' 'networkmanager' 'xorg-setxkbmap') source=() diff --git a/raveos-calamares-module/raveusers/CMakeLists.txt b/raveos-calamares-module/raveusers/CMakeLists.txt new file mode 100644 index 0000000..680056a --- /dev/null +++ b/raveos-calamares-module/raveusers/CMakeLists.txt @@ -0,0 +1,37 @@ +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Quick QuickWidgets) + +calamares_add_plugin(raveusers + TYPE viewmodule + EXPORT_MACRO PLUGINDLLEXPORT_PRO + SOURCES + RaveUsersViewStep.cpp + RaveUsersBackend.cpp + RaveUsersJobs.cpp + LINK_LIBRARIES + Qt6::Core + Qt6::Widgets + Qt6::Quick + Qt6::QuickWidgets + Calamares::calamares + crypt +) + +install( + FILES module.desc + DESTINATION lib/calamares/modules/raveusers +) + +install( + FILES raveusers.conf + DESTINATION share/calamares/modules +) + +install( + FILES + ui/raveusers.qml + ui/PasswordField.qml + ui/Translations.qml + ui/qmldir + ui/raveos-logo.png + DESTINATION lib/calamares/modules/raveusers/ui +) diff --git a/raveos-calamares-module/raveusers/RaveUsersBackend.cpp b/raveos-calamares-module/raveusers/RaveUsersBackend.cpp new file mode 100644 index 0000000..6f8eea6 --- /dev/null +++ b/raveos-calamares-module/raveusers/RaveUsersBackend.cpp @@ -0,0 +1,242 @@ +#include "RaveUsersBackend.h" + +#include <QRegularExpression> + +namespace +{ +const QRegularExpression USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); +constexpr int USERNAME_MAX_LENGTH = 31; + +const QRegularExpression HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); +constexpr int HOSTNAME_MIN_LENGTH = 2; +constexpr int HOSTNAME_MAX_LENGTH = 63; +} + +RaveUsersBackend::RaveUsersBackend( QObject* parent ) + : QObject( parent ) + , m_forbiddenLoginNames { QStringLiteral( "root" ), QStringLiteral( "nobody" ) } + , m_forbiddenHostNames { QStringLiteral( "localhost" ) } +{ + m_defaultGroups = { "lp", "video", "network", "storage", "wheel", "audio" }; +} + +QString +RaveUsersBackend::usernameStatus() const +{ + if ( m_username.isEmpty() ) + { + return QString(); + } + if ( m_username.length() > USERNAME_MAX_LENGTH ) + { + return tr( "Your username is too long." ); + } + if ( m_username.indexOf( QRegularExpression( "^[a-z_]" ) ) != 0 ) + { + return tr( "Your username must start with a lowercase letter or underscore." ); + } + if ( m_username.indexOf( USERNAME_RX ) != 0 ) + { + return tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ); + } + if ( m_forbiddenLoginNames.contains( m_username, Qt::CaseInsensitive ) ) + { + return tr( "'%1' is not allowed as username." ).arg( m_username ); + } + return QString(); +} + +QString +RaveUsersBackend::hostnameStatus() const +{ + if ( m_hostname.isEmpty() ) + { + return QString(); + } + if ( m_hostname.length() < HOSTNAME_MIN_LENGTH ) + { + return tr( "Your hostname is too short." ); + } + if ( m_hostname.length() > HOSTNAME_MAX_LENGTH ) + { + return tr( "Your hostname is too long." ); + } + if ( m_forbiddenHostNames.contains( m_hostname, Qt::CaseInsensitive ) ) + { + return tr( "'%1' is not allowed as hostname." ).arg( m_hostname ); + } + if ( m_hostname.indexOf( HOSTNAME_RX ) != 0 ) + { + return tr( "Only letters, numbers, underscore and hyphen are allowed." ); + } + return QString(); +} + +QString +RaveUsersBackend::passwordStatus() const +{ + if ( m_password.isEmpty() ) + { + return QString(); + } + if ( m_password != m_passwordSecondary ) + { + return tr( "Your passwords do not match!" ); + } + return QString(); +} + +QString +RaveUsersBackend::rootPasswordStatus() const +{ + if ( m_reuseRootPassword ) + { + return QString(); + } + if ( m_rootPassword.isEmpty() ) + { + return QString(); + } + if ( m_rootPassword != m_rootPasswordSecondary ) + { + return tr( "Your root passwords do not match!" ); + } + return QString(); +} + +bool +RaveUsersBackend::isReady() const +{ + if ( m_username.isEmpty() || !usernameStatus().isEmpty() ) + { + return false; + } + if ( m_hostname.isEmpty() || !hostnameStatus().isEmpty() ) + { + return false; + } + if ( m_password.isEmpty() || !passwordStatus().isEmpty() ) + { + return false; + } + if ( m_setRootPassword && !m_reuseRootPassword ) + { + if ( m_rootPassword.isEmpty() || !rootPasswordStatus().isEmpty() ) + { + return false; + } + } + return true; +} + +QStringList +RaveUsersBackend::groupsForThisUser() const +{ + QStringList groups = m_defaultGroups; + groups.removeDuplicates(); + return groups; +} + +void +RaveUsersBackend::setUsername( const QString& s ) +{ + if ( s != m_username ) + { + m_username = s; + emit usernameChanged(); + emit statusChanged(); + + if ( !m_customHostname ) + { + const QString generated = s.isEmpty() ? QString() : s + QStringLiteral( "-RaveOS-PC" ); + if ( generated != m_hostname ) + { + m_hostname = generated; + emit hostnameChanged(); + emit statusChanged(); + } + } + + checkReady(); + } +} + +void +RaveUsersBackend::setHostname( const QString& s ) +{ + m_customHostname = true; + if ( s != m_hostname ) + { + m_hostname = s; + emit hostnameChanged(); + emit statusChanged(); + checkReady(); + } +} + +void +RaveUsersBackend::setPassword( const QString& s ) +{ + if ( s != m_password ) + { + m_password = s; + emit statusChanged(); + checkReady(); + } +} + +void +RaveUsersBackend::setPasswordSecondary( const QString& s ) +{ + if ( s != m_passwordSecondary ) + { + m_passwordSecondary = s; + emit statusChanged(); + checkReady(); + } +} + +void +RaveUsersBackend::setRootPasswordText( const QString& s ) +{ + if ( s != m_rootPassword ) + { + m_rootPassword = s; + emit statusChanged(); + checkReady(); + } +} + +void +RaveUsersBackend::setRootPasswordSecondaryText( const QString& s ) +{ + if ( s != m_rootPasswordSecondary ) + { + m_rootPasswordSecondary = s; + emit statusChanged(); + checkReady(); + } +} + +void +RaveUsersBackend::setReuseRootPassword( bool b ) +{ + if ( b != m_reuseRootPassword ) + { + m_reuseRootPassword = b; + emit reuseRootPasswordChanged(); + emit statusChanged(); + checkReady(); + } +} + +void +RaveUsersBackend::checkReady() +{ + const bool ready = isReady(); + if ( ready != m_lastReady ) + { + m_lastReady = ready; + emit readyChanged(); + } +} diff --git a/raveos-calamares-module/raveusers/RaveUsersBackend.h b/raveos-calamares-module/raveusers/RaveUsersBackend.h new file mode 100644 index 0000000..4e06f47 --- /dev/null +++ b/raveos-calamares-module/raveusers/RaveUsersBackend.h @@ -0,0 +1,89 @@ +#pragma once + +#include <QObject> +#include <QString> +#include <QStringList> + +class RaveUsersBackend : public QObject +{ + Q_OBJECT + + Q_PROPERTY( QString username READ username NOTIFY usernameChanged ) + Q_PROPERTY( QString hostname READ hostname NOTIFY hostnameChanged ) + Q_PROPERTY( QString usernameStatus READ usernameStatus NOTIFY statusChanged ) + Q_PROPERTY( QString hostnameStatus READ hostnameStatus NOTIFY statusChanged ) + Q_PROPERTY( QString passwordStatus READ passwordStatus NOTIFY statusChanged ) + Q_PROPERTY( QString rootPasswordStatus READ rootPasswordStatus NOTIFY statusChanged ) + Q_PROPERTY( bool ready READ isReady NOTIFY readyChanged ) + Q_PROPERTY( bool reuseRootPassword READ reuseRootPassword NOTIFY reuseRootPasswordChanged ) + Q_PROPERTY( bool rootReuseVisible READ rootReuseVisible CONSTANT ) + +public: + explicit RaveUsersBackend( QObject* parent = nullptr ); + + QString username() const { return m_username; } + QString hostname() const { return m_hostname; } + QString usernameStatus() const; + QString hostnameStatus() const; + QString passwordStatus() const; + QString rootPasswordStatus() const; + bool isReady() const; + bool reuseRootPassword() const { return m_reuseRootPassword; } + bool rootReuseVisible() const { return m_setRootPassword; } + + QString userPassword() const { return m_password; } + QString rootPassword() const { return m_reuseRootPassword ? m_password : m_rootPassword; } + + QString userShell() const { return m_userShell; } + QStringList groupsForThisUser() const; + QString sudoersGroup() const { return m_sudoersGroup; } + bool sudoersConfigureWithGroup() const { return m_sudoersConfigureWithGroup; } + int homeUMask() const { return m_homeUMask; } + QStringList defaultGroups() const { return m_defaultGroups; } + + Q_INVOKABLE void setUsername( const QString& s ); + Q_INVOKABLE void setHostname( const QString& s ); + Q_INVOKABLE void setPassword( const QString& s ); + Q_INVOKABLE void setPasswordSecondary( const QString& s ); + Q_INVOKABLE void setRootPasswordText( const QString& s ); + Q_INVOKABLE void setRootPasswordSecondaryText( const QString& s ); + Q_INVOKABLE void setReuseRootPassword( bool b ); + + void setForbiddenLoginNames( const QStringList& names ) { m_forbiddenLoginNames = names; } + void setForbiddenHostNames( const QStringList& names ) { m_forbiddenHostNames = names; } + void setUserShell( const QString& shell ) { m_userShell = shell; } + void setSudoersGroup( const QString& group ) { m_sudoersGroup = group; } + void setSudoersConfigureWithGroup( bool b ) { m_sudoersConfigureWithGroup = b; } + void setSetRootPassword( bool b ) { m_setRootPassword = b; } + void setHomeUMask( int umask ) { m_homeUMask = umask; } + void setDefaultGroups( const QStringList& groups ) { m_defaultGroups = groups; } + +signals: + void usernameChanged(); + void hostnameChanged(); + void statusChanged(); + void readyChanged(); + void reuseRootPasswordChanged(); + +private: + void checkReady(); + + QString m_username; + QString m_hostname; + QString m_password; + QString m_passwordSecondary; + QString m_rootPassword; + QString m_rootPasswordSecondary; + bool m_customHostname = false; + bool m_reuseRootPassword = true; + bool m_setRootPassword = true; + + QStringList m_forbiddenLoginNames; + QStringList m_forbiddenHostNames; + QString m_userShell = QStringLiteral( "/bin/bash" ); + QString m_sudoersGroup; + bool m_sudoersConfigureWithGroup = true; + int m_homeUMask = -1; + QStringList m_defaultGroups; + bool m_lastReady = false; +}; diff --git a/raveos-calamares-module/raveusers/RaveUsersJobs.cpp b/raveos-calamares-module/raveusers/RaveUsersJobs.cpp new file mode 100644 index 0000000..1923c38 --- /dev/null +++ b/raveos-calamares-module/raveusers/RaveUsersJobs.cpp @@ -0,0 +1,294 @@ +#include "RaveUsersJobs.h" + +#include "RaveUsersBackend.h" + +#include "GlobalStorage.h" +#include "JobQueue.h" +#include "utils/Logger.h" +#include "utils/Permissions.h" +#include "utils/System.h" + +#include <QDir> +#include <QFile> +#include <QRandomGenerator> +#include <QSet> + +#include <crypt.h> + +using WriteMode = Calamares::System::WriteMode; + +static QDir +targetRootDir() +{ + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + return QDir( gs->value( "rootMountPoint" ).toString() ); +} + +RaveUsersCreateUserJob::RaveUsersCreateUserJob( const RaveUsersBackend* backend ) + : m_backend( backend ) +{ +} + +QString +RaveUsersCreateUserJob::prettyName() const +{ + return tr( "Create user %1" ).arg( m_backend->username() ); +} + +QString +RaveUsersCreateUserJob::prettyStatusMessage() const +{ + return tr( "Creating user %1…", "@status" ).arg( m_backend->username() ); +} + +Calamares::JobResult +RaveUsersCreateUserJob::exec() +{ + const QString login = m_backend->username(); + + QStringList useraddCommand { "useradd", "-m", "-U" }; + const QString shell = m_backend->userShell(); + if ( !shell.isEmpty() ) + { + useraddCommand << "-s" << shell; + } + + useraddCommand << "-c" << login; + if ( m_backend->homeUMask() >= 0 ) + { + useraddCommand << "-K" << QStringLiteral( "UMASK=%1" ).arg( m_backend->homeUMask(), 3, 8, QChar( '0' ) ); + } + useraddCommand << login; + + auto result = Calamares::System::instance()->targetEnvCommand( useraddCommand ); + if ( result.getExitCode() ) + { + cError() << "useradd failed" << result.getExitCode(); + return result.explainProcess( useraddCommand, std::chrono::seconds( 10 ) ); + } + + const QStringList groups = m_backend->groupsForThisUser(); + if ( !groups.isEmpty() ) + { + QStringList usermodCommand { "usermod", "-aG", groups.join( ',' ), login }; + result = Calamares::System::instance()->targetEnvCommand( usermodCommand ); + if ( result.getExitCode() ) + { + cError() << "usermod failed" << result.getExitCode(); + return result.explainProcess( usermodCommand, std::chrono::seconds( 10 ) ); + } + } + + const QString userGroup = QStringLiteral( "%1:%2" ).arg( login, login ); + const QString homeDir = QStringLiteral( "/home/%1" ).arg( login ); + result = Calamares::System::instance()->targetEnvCommand( { "chown", "-R", userGroup, homeDir } ); + if ( result.getExitCode() ) + { + cError() << "chown failed" << result.getExitCode(); + return result.explainProcess( "chown", std::chrono::seconds( 10 ) ); + } + + return Calamares::JobResult::ok(); +} + +RaveUsersSetPasswordJob::RaveUsersSetPasswordJob( const QString& userName, const QString& password ) + : m_userName( userName ) + , m_password( password ) +{ +} + +QString +RaveUsersSetPasswordJob::prettyName() const +{ + return tr( "Set password for user %1" ).arg( m_userName ); +} + +QString +RaveUsersSetPasswordJob::prettyStatusMessage() const +{ + return tr( "Setting password for user %1…", "@status" ).arg( m_userName ); +} + +static QString +makeSha512Salt( int length = 16 ) +{ + static const QString chars = QStringLiteral( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./" ); + QString salt; + salt.reserve( length ); + for ( int i = 0; i < length; ++i ) + { + salt += chars.at( QRandomGenerator::global()->bounded( chars.size() ) ); + } + return QStringLiteral( "$6$" ) + salt + QLatin1Char( '$' ); +} + +Calamares::JobResult +RaveUsersSetPasswordJob::exec() +{ + QDir destDir = targetRootDir(); + if ( !destDir.exists() ) + { + return Calamares::JobResult::error( tr( "Bad destination system path." ), + tr( "rootMountPoint is %1" ).arg( destDir.absolutePath() ) ); + } + + if ( m_userName == QLatin1String( "root" ) && m_password.isEmpty() ) + { + int ec = Calamares::System::instance()->targetEnvCall( { "usermod", "-p", "!", m_userName } ); + if ( ec ) + { + return Calamares::JobResult::error( tr( "Cannot disable root account." ), + tr( "usermod terminated with error code %1." ).arg( ec ) ); + } + return Calamares::JobResult::ok(); + } + + const QString salt = makeSha512Salt(); + const QByteArray hash = crypt( m_password.toUtf8().constData(), salt.toUtf8().constData() ); + if ( hash.isEmpty() ) + { + return Calamares::JobResult::error( tr( "Cannot hash password." ) ); + } + + int ec = Calamares::System::instance()->targetEnvCall( + { "usermod", "-p", QString::fromLatin1( hash ), m_userName } ); + if ( ec ) + { + return Calamares::JobResult::error( tr( "Cannot set password for user %1." ).arg( m_userName ), + tr( "usermod terminated with error code %1." ).arg( ec ) ); + } + + return Calamares::JobResult::ok(); +} + +RaveUsersSetHostnameJob::RaveUsersSetHostnameJob( const QString& hostname ) + : m_hostname( hostname ) +{ +} + +QString +RaveUsersSetHostnameJob::prettyName() const +{ + return tr( "Set hostname %1" ).arg( m_hostname ); +} + +Calamares::JobResult +RaveUsersSetHostnameJob::exec() +{ + if ( !Calamares::System::instance()->createTargetFile( QStringLiteral( "/etc/hostname" ), + ( m_hostname + '\n' ).toUtf8(), + WriteMode::Overwrite ) ) + { + return Calamares::JobResult::error( tr( "Cannot write hostname to target system" ) ); + } + + const QString hosts = QStringLiteral( R"(# Standard host addresses +127.0.0.1 localhost +::1 localhost ip6-localhost ip6-loopback +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +# This host address +127.0.1.1 %1 +)" ) + .arg( m_hostname ); + + if ( !Calamares::System::instance()->createTargetFile( + QStringLiteral( "/etc/hosts" ), hosts.toUtf8(), WriteMode::Overwrite ) ) + { + return Calamares::JobResult::error( tr( "Cannot write hosts file to target system" ) ); + } + + return Calamares::JobResult::ok(); +} + +RaveUsersSetupSudoJob::RaveUsersSetupSudoJob( const QString& group, bool configureWithGroup ) + : m_group( group ) + , m_configureWithGroup( configureWithGroup ) +{ +} + +QString +RaveUsersSetupSudoJob::prettyName() const +{ + return tr( "Configuring sudo users…", "@status" ); +} + +Calamares::JobResult +RaveUsersSetupSudoJob::exec() +{ + if ( m_group.isEmpty() ) + { + return Calamares::JobResult::ok(); + } + + const QString designator = m_configureWithGroup ? QStringLiteral( "(ALL:ALL)" ) : QStringLiteral( "(ALL)" ); + const QString line = QChar( '%' ) + QStringLiteral( "%1 ALL=%2 ALL\n" ).arg( m_group, designator ); + + auto fileResult = Calamares::System::instance()->createTargetFile( + QStringLiteral( "/etc/sudoers.d/10-installer" ), line.toUtf8().constData(), WriteMode::Overwrite ); + + if ( fileResult ) + { + if ( !Calamares::Permissions::apply( fileResult.path(), 0440 ) ) + { + return Calamares::JobResult::error( tr( "Cannot chmod sudoers file." ) ); + } + } + else + { + return Calamares::JobResult::error( tr( "Cannot create sudoers file for writing." ) ); + } + + return Calamares::JobResult::ok(); +} + +RaveUsersSetupGroupsJob::RaveUsersSetupGroupsJob( const QStringList& groups ) + : m_groups( groups ) +{ +} + +QString +RaveUsersSetupGroupsJob::prettyName() const +{ + return tr( "Preparing groups…", "@status" ); +} + +Calamares::JobResult +RaveUsersSetupGroupsJob::exec() +{ + QDir targetRoot = targetRootDir(); + + QSet< QString > existing; + QFile groupFile( targetRoot.absoluteFilePath( "etc/group" ) ); + if ( groupFile.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { + while ( !groupFile.atEnd() ) + { + const QString line = QString::fromLocal8Bit( groupFile.readLine() ); + if ( line.startsWith( '#' ) ) + { + continue; + } + const int idx = line.indexOf( ':' ); + if ( idx > 0 ) + { + existing.insert( line.left( idx ) ); + } + } + } + + for ( const QString& group : m_groups ) + { + if ( group.isEmpty() || existing.contains( group ) ) + { + continue; + } + QStringList cmd { "groupadd", "--system", group }; + if ( Calamares::System::instance()->targetEnvCall( cmd ) ) + { + cWarning() << "groupadd failed for" << group; + } + } + + return Calamares::JobResult::ok(); +} diff --git a/raveos-calamares-module/raveusers/RaveUsersJobs.h b/raveos-calamares-module/raveusers/RaveUsersJobs.h new file mode 100644 index 0000000..c98a0b2 --- /dev/null +++ b/raveos-calamares-module/raveusers/RaveUsersJobs.h @@ -0,0 +1,67 @@ +#pragma once + +#include <Job.h> + +#include <QString> +#include <QStringList> + +class RaveUsersBackend; + +class RaveUsersCreateUserJob : public Calamares::Job +{ +public: + explicit RaveUsersCreateUserJob( const RaveUsersBackend* backend ); + QString prettyName() const override; + QString prettyStatusMessage() const override; + Calamares::JobResult exec() override; + +private: + const RaveUsersBackend* m_backend; +}; + +class RaveUsersSetPasswordJob : public Calamares::Job +{ +public: + RaveUsersSetPasswordJob( const QString& userName, const QString& password ); + QString prettyName() const override; + QString prettyStatusMessage() const override; + Calamares::JobResult exec() override; + +private: + QString m_userName; + QString m_password; +}; + +class RaveUsersSetHostnameJob : public Calamares::Job +{ +public: + explicit RaveUsersSetHostnameJob( const QString& hostname ); + QString prettyName() const override; + Calamares::JobResult exec() override; + +private: + QString m_hostname; +}; + +class RaveUsersSetupSudoJob : public Calamares::Job +{ +public: + RaveUsersSetupSudoJob( const QString& group, bool configureWithGroup ); + QString prettyName() const override; + Calamares::JobResult exec() override; + +private: + QString m_group; + bool m_configureWithGroup; +}; + +class RaveUsersSetupGroupsJob : public Calamares::Job +{ +public: + explicit RaveUsersSetupGroupsJob( const QStringList& groups ); + QString prettyName() const override; + Calamares::JobResult exec() override; + +private: + QStringList m_groups; +}; diff --git a/raveos-calamares-module/raveusers/RaveUsersViewStep.cpp b/raveos-calamares-module/raveusers/RaveUsersViewStep.cpp new file mode 100644 index 0000000..4e21e65 --- /dev/null +++ b/raveos-calamares-module/raveusers/RaveUsersViewStep.cpp @@ -0,0 +1,253 @@ +#include "RaveUsersViewStep.h" + +#include "RaveUsersBackend.h" +#include "RaveUsersJobs.h" + +#include "GlobalStorage.h" +#include "JobQueue.h" +#include "utils/Logger.h" +#include "utils/String.h" +#include "utils/Variant.h" + +#include <QDir> +#include <QFile> +#include <QLocale> +#include <QQmlContext> +#include <QQmlEngine> +#include <QQuickItem> +#include <QQuickWidget> +#include <QVBoxLayout> + +CALAMARES_PLUGIN_FACTORY_DEFINITION( RaveUsersViewStepFactory, registerPlugin< RaveUsersViewStep >(); ) + +RaveUsersViewStep::RaveUsersViewStep( QObject* parent ) + : Calamares::ViewStep( parent ) + , m_backend( new RaveUsersBackend( this ) ) +{ + connect( m_backend, &RaveUsersBackend::readyChanged, this, [this]() { + emit nextStatusChanged( m_backend->isReady() ); + } ); +} + +RaveUsersViewStep::~RaveUsersViewStep() +{ + if ( m_widget && m_widget->parent() == nullptr ) + { + delete m_widget; + } +} + +QString +RaveUsersViewStep::prettyName() const +{ + return tr( "User" ); +} + +void +RaveUsersViewStep::buildWidget() +{ + m_widget = new QWidget(); + auto* layout = new QVBoxLayout( m_widget ); + layout->setContentsMargins( 0, 0, 0, 0 ); + + m_quickWidget = new QQuickWidget(); + m_quickWidget->setResizeMode( QQuickWidget::SizeRootObjectToView ); + + m_quickWidget->setClearColor( QColor( 0x40, 0x40, 0x40 ) ); + + QString modulePath = QStringLiteral( "/usr/lib/calamares/modules/raveusers/ui" ); + if ( !QDir( modulePath ).exists() ) + { + modulePath = QDir::currentPath() + QStringLiteral( "/raveusers/ui" ); + } + + m_quickWidget->engine()->addImportPath( modulePath ); + m_quickWidget->rootContext()->setContextProperty( "backend", m_backend ); + + m_locale = getLocale(); + m_quickWidget->rootContext()->setContextProperty( "systemLocale", m_locale ); + + QString qmlPath = QStringLiteral( "/usr/lib/calamares/modules/raveusers/ui/raveusers.qml" ); + if ( !QFile::exists( qmlPath ) ) + { + qmlPath = QDir::currentPath() + QStringLiteral( "/raveusers/ui/raveusers.qml" ); + } + + m_quickWidget->setSource( QUrl::fromLocalFile( qmlPath ) ); + layout->addWidget( m_quickWidget ); +} + +QWidget* +RaveUsersViewStep::widget() +{ + if ( !m_widget ) + { + buildWidget(); + } + return m_widget; +} + +bool +RaveUsersViewStep::isNextEnabled() const +{ + return m_backend && m_backend->isReady(); +} + +bool +RaveUsersViewStep::isBackEnabled() const +{ + return true; +} + +bool +RaveUsersViewStep::isAtBeginning() const +{ + return true; +} + +bool +RaveUsersViewStep::isAtEnd() const +{ + return true; +} + +Calamares::JobList +RaveUsersViewStep::jobs() const +{ + Calamares::JobList jobs; + + if ( !m_backend || !m_backend->isReady() ) + { + return jobs; + } + + if ( !m_backend->sudoersGroup().isEmpty() ) + { + jobs.append( Calamares::job_ptr( + new RaveUsersSetupSudoJob( m_backend->sudoersGroup(), m_backend->sudoersConfigureWithGroup() ) ) ); + } + + jobs.append( Calamares::job_ptr( new RaveUsersSetupGroupsJob( m_backend->defaultGroups() ) ) ); + jobs.append( Calamares::job_ptr( new RaveUsersCreateUserJob( m_backend ) ) ); + jobs.append( Calamares::job_ptr( new RaveUsersSetPasswordJob( m_backend->username(), m_backend->userPassword() ) ) ); + jobs.append( Calamares::job_ptr( new RaveUsersSetPasswordJob( QStringLiteral( "root" ), m_backend->rootPassword() ) ) ); + jobs.append( Calamares::job_ptr( new RaveUsersSetHostnameJob( m_backend->hostname() ) ) ); + + return jobs; +} + +void +RaveUsersViewStep::onActivate() +{ + const QString newLocale = getLocale(); + if ( newLocale != m_locale ) + { + m_locale = newLocale; + if ( m_quickWidget ) + { + m_quickWidget->rootContext()->setContextProperty( "systemLocale", m_locale ); + } + } + + if ( m_quickWidget ) + { + QQuickItem* root = m_quickWidget->rootObject(); + if ( root ) + { + QMetaObject::invokeMethod( root, "updateLanguage", Qt::DirectConnection ); + } + } +} + +void +RaveUsersViewStep::onLeave() +{ + if ( !m_backend || !m_backend->isReady() ) + { + return; + } + + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( !gs ) + { + return; + } + + const QString username = m_backend->username(); + gs->insert( "username", username ); + gs->insert( "hostname", m_backend->hostname() ); + gs->insert( "fullname", username ); + gs->insert( "password", Calamares::String::obscure( m_backend->userPassword() ) ); + gs->insert( "setRootPassword", m_backend->rootReuseVisible() ); + gs->insert( "reuseRootPassword", m_backend->reuseRootPassword() ); +} + +void +RaveUsersViewStep::setConfigurationMap( const QVariantMap& config ) +{ + const QVariantMap userSettings = config.value( "user" ).toMap(); + QString shell = Calamares::getString( userSettings, "shell" ); + m_backend->setUserShell( shell.isEmpty() ? QStringLiteral( "/bin/bash" ) : shell ); + + QStringList forbiddenLogin = Calamares::getStringList( userSettings, "forbidden_names" ); + forbiddenLogin << QStringLiteral( "root" ) << QStringLiteral( "nobody" ); + forbiddenLogin.removeDuplicates(); + m_backend->setForbiddenLoginNames( forbiddenLogin ); + + const QVariantMap hostnameSettings = config.value( "hostname" ).toMap(); + QStringList forbiddenHost = Calamares::getStringList( hostnameSettings, "forbidden_names" ); + forbiddenHost << QStringLiteral( "localhost" ); + forbiddenHost.removeDuplicates(); + m_backend->setForbiddenHostNames( forbiddenHost ); + + m_backend->setSudoersGroup( Calamares::getString( config, "sudoersGroup" ) ); + m_backend->setSudoersConfigureWithGroup( Calamares::getBool( config, "sudoersConfigureWithGroup", true ) ); + m_backend->setSetRootPassword( Calamares::getBool( config, "setRootPassword", true ) ); + + QStringList groups; + for ( const auto& v : config.value( "defaultGroups" ).toList() ) + { + if ( v.typeId() == QMetaType::QString ) + { + groups << v.toString(); + } + else if ( v.typeId() == QMetaType::QVariantMap ) + { + const QString name = v.toMap().value( "name" ).toString(); + if ( !name.isEmpty() ) + { + groups << name; + } + } + } + if ( !groups.isEmpty() ) + { + m_backend->setDefaultGroups( groups ); + } +} + +QString +RaveUsersViewStep::getLocale() const +{ + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( gs ) + { + if ( gs->contains( "locale" ) ) + { + const QString loc = gs->value( "locale" ).toString(); + if ( !loc.isEmpty() ) + { + return loc; + } + } + if ( gs->contains( "localeConf" ) ) + { + const QString lang = gs->value( "localeConf" ).toMap().value( "LANG" ).toString(); + if ( !lang.isEmpty() ) + { + return lang; + } + } + } + return QLocale::system().name(); +} diff --git a/raveos-calamares-module/raveusers/RaveUsersViewStep.h b/raveos-calamares-module/raveusers/RaveUsersViewStep.h new file mode 100644 index 0000000..67593ac --- /dev/null +++ b/raveos-calamares-module/raveusers/RaveUsersViewStep.h @@ -0,0 +1,47 @@ +#pragma once + +#include <QObject> +#include <QString> +#include <QStringList> +#include <QWidget> + +#include <Job.h> +#include <utils/PluginFactory.h> +#include <viewpages/ViewStep.h> + +class QQuickWidget; +class RaveUsersBackend; + +class PLUGINDLLEXPORT RaveUsersViewStep : public Calamares::ViewStep +{ + Q_OBJECT + +public: + explicit RaveUsersViewStep( QObject* parent = nullptr ); + ~RaveUsersViewStep() 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 setConfigurationMap( const QVariantMap& configurationMap ) override; + void onActivate() override; + void onLeave() override; + +private: + void buildWidget(); + QString getLocale() const; + + QWidget* m_widget = nullptr; + QQuickWidget* m_quickWidget = nullptr; + RaveUsersBackend* m_backend = nullptr; + QString m_locale; +}; + +CALAMARES_PLUGIN_FACTORY_DECLARATION( RaveUsersViewStepFactory ) diff --git a/raveos-calamares-module/raveusers/module.desc b/raveos-calamares-module/raveusers/module.desc new file mode 100644 index 0000000..143b7ec --- /dev/null +++ b/raveos-calamares-module/raveusers/module.desc @@ -0,0 +1,5 @@ +--- +type: "viewmodule" +name: "raveusers" +interface: "qtplugin" +load: "libcalamares_viewmodule_raveusers.so" diff --git a/raveos-calamares-module/raveusers/raveusers.conf b/raveos-calamares-module/raveusers/raveusers.conf new file mode 100644 index 0000000..8985cbe --- /dev/null +++ b/raveos-calamares-module/raveusers/raveusers.conf @@ -0,0 +1,26 @@ +--- +defaultGroups: + - name: users + must_exist: true + system: true + - lp + - video + - network + - storage + - name: wheel + must_exist: false + system: true + - audio + - plugdev + +sudoersGroup: wheel +sudoersConfigureWithGroup: true + +setRootPassword: true + +user: + shell: /bin/bash + forbidden_names: [ root ] + +hostname: + forbidden_names: [ localhost ] diff --git a/raveos-calamares-module/raveusers/ui/PasswordField.qml b/raveos-calamares-module/raveusers/ui/PasswordField.qml new file mode 100644 index 0000000..0dffb49 --- /dev/null +++ b/raveos-calamares-module/raveusers/ui/PasswordField.qml @@ -0,0 +1,41 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +TextField { + id: field + property bool reveal: false + signal revealClicked() + + implicitHeight: 52 + leftPadding: 14 + rightPadding: 46 + echoMode: reveal ? TextInput.Normal : TextInput.Password + color: "#ffffff" + placeholderTextColor: "#5b6270" + font.pixelSize: 15 + + background: Rectangle { + radius: 7 + color: "#303030" + border.color: field.activeFocus ? "#3d7839" : "#303030" + border.width: field.activeFocus ? 1.5 : 1 + } + + Text { + text: "\uD83D\uDC41" + font.pixelSize: 16 + color: field.reveal ? "#3d7839" : "#9ca3af" + anchors.right: parent.right + anchors.rightMargin: 14 + anchors.verticalCenter: parent.verticalCenter + } + + MouseArea { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: 40 + height: 40 + cursorShape: Qt.PointingHandCursor + onClicked: field.revealClicked() + } +} diff --git a/raveos-calamares-module/raveusers/ui/Translations.qml b/raveos-calamares-module/raveusers/ui/Translations.qml new file mode 100644 index 0000000..cb267b4 --- /dev/null +++ b/raveos-calamares-module/raveusers/ui/Translations.qml @@ -0,0 +1,58 @@ +pragma Singleton +import QtQuick 2.15 + +QtObject { + id: root + + property string title: "Create user" + property string description: "Set up the computer's primary user account. This account has full administrator (root / sudo) privileges." + property string usernameLabel: "Username" + property string hostnameLabel: "Computer name" + property string passwordLabel: "Password" + property string passwordPlaceholder: "Password" + property string passwordConfirmLabel: "Confirm password" + property string reuseRootLabel: "Use the same password for the administrator (root)" + property string rootPasswordLabel: "Root password" + property string rootPasswordPlaceholder: "Root password" + property string rootPasswordConfirmLabel: "Confirm root password" + + function setLanguage(locale) { + if (locale.startsWith("hu")) { + title = "Felhasználó létrehozása" + description = "Állítsd be a számítógép elsődleges felhasználói fiókját. Ez a fiók teljes rendszergazdai (root / sudo) jogosultsággal rendelkezik." + usernameLabel = "Felhasználónév" + hostnameLabel = "Számítógépnév" + passwordLabel = "Jelszó" + passwordPlaceholder = "Jelszó" + passwordConfirmLabel = "Jelszó megerősítése" + reuseRootLabel = "Ugyanaz a jelszó az adminisztrátorhoz (root)" + rootPasswordLabel = "Root jelszó" + rootPasswordPlaceholder = "Root jelszó" + rootPasswordConfirmLabel = "Root jelszó megerősítése" + } else if (locale.startsWith("de")) { + title = "Benutzer erstellen" + description = "Richte das primäre Benutzerkonto des Computers ein. Dieses Konto hat volle Administratorrechte (root / sudo)." + usernameLabel = "Benutzername" + hostnameLabel = "Computername" + passwordLabel = "Passwort" + passwordPlaceholder = "Passwort" + passwordConfirmLabel = "Passwort bestätigen" + reuseRootLabel = "Dasselbe Passwort für den Administrator (root)" + rootPasswordLabel = "Root-Passwort" + rootPasswordPlaceholder = "Root-Passwort" + rootPasswordConfirmLabel = "Root-Passwort bestätigen" + } else { + title = "Create user" + description = "Set up the computer's primary user account. This account has full administrator (root / sudo) privileges." + usernameLabel = "Username" + hostnameLabel = "Computer name" + passwordLabel = "Password" + passwordPlaceholder = "Password" + passwordConfirmLabel = "Confirm password" + reuseRootLabel = "Use the same password for the administrator (root)" + rootPasswordLabel = "Root password" + rootPasswordPlaceholder = "Root password" + rootPasswordConfirmLabel = "Confirm root password" + } + } +} diff --git a/raveos-calamares-module/raveusers/ui/qmldir b/raveos-calamares-module/raveusers/ui/qmldir new file mode 100644 index 0000000..c87b787 --- /dev/null +++ b/raveos-calamares-module/raveusers/ui/qmldir @@ -0,0 +1,2 @@ +module RaveUsersModule +singleton Translations 1.0 Translations.qml diff --git a/raveos-calamares-module/raveusers/ui/raveos-logo.png b/raveos-calamares-module/raveusers/ui/raveos-logo.png Binary files differnew file mode 100755 index 0000000..9903765 --- /dev/null +++ b/raveos-calamares-module/raveusers/ui/raveos-logo.png diff --git a/raveos-calamares-module/raveusers/ui/raveusers.qml b/raveos-calamares-module/raveusers/ui/raveusers.qml new file mode 100644 index 0000000..25e090f --- /dev/null +++ b/raveos-calamares-module/raveusers/ui/raveusers.qml @@ -0,0 +1,286 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "." as RaveUsersModule + +Rectangle { + id: root + color: "#2B2B2B" + radius: 14 + + readonly property color cCardBg: "#20232a" + readonly property color cCardBorder: "#333947" + readonly property color cInputBg: "#303030" + readonly property color cInputBorder: "#303030" + readonly property color cAccentGreen: "#3d7839" + readonly property color cTextWhite: "#ffffff" + readonly property color cTextMuted: "#8b93a3" + readonly property color cTextFaint: "#5b6270" + readonly property color cErrorRed: "#ff6b6b" + + property bool showUserPassword: false + property bool showRootPassword: false + + function updateLanguage() { + RaveUsersModule.Translations.setLanguage(systemLocale) + } + + Component.onCompleted: { + RaveUsersModule.Translations.setLanguage(systemLocale) + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 40 + spacing: 0 + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 10 + width: Math.min(parent.width - 40, 540) + spacing: 16 + + Image { + source: "raveos-logo.png" + sourceSize.width: 96 + sourceSize.height: 96 + fillMode: Image.PreserveAspectFit + Layout.preferredWidth: 96 + Layout.preferredHeight: 96 + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: RaveUsersModule.Translations.title + font.pixelSize: 28 + font.bold: true + color: cTextWhite + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: RaveUsersModule.Translations.description + font.pixelSize: 15 + lineHeight: 1.45 + color: cTextMuted + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + } + + Item { height: 6 } + + ColumnLayout { + Layout.fillWidth: true + spacing: 6 + + Text { + text: RaveUsersModule.Translations.usernameLabel + color: "#c7ccd6" + font.pixelSize: 14 + font.weight: Font.Medium + } + + TextField { + id: usernameField + Layout.fillWidth: true + implicitHeight: 52 + leftPadding: 14 + rightPadding: 14 + text: backend.username + placeholderText: "raveuser" + color: cTextWhite + placeholderTextColor: "#5b6270" + font.pixelSize: 15 + background: Rectangle { + radius: 7 + color: cInputBg + border.color: usernameField.activeFocus ? cAccentGreen : cInputBorder + border.width: usernameField.activeFocus ? 1.5 : 1 + } + onTextEdited: backend.setUsername(text) + } + + Text { + visible: backend.usernameStatus !== "" + text: backend.usernameStatus + color: cErrorRed + font.pixelSize: 12 + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 6 + + Text { + text: RaveUsersModule.Translations.hostnameLabel + color: "#c7ccd6" + font.pixelSize: 14 + font.weight: Font.Medium + } + + TextField { + id: hostnameField + Layout.fillWidth: true + implicitHeight: 52 + leftPadding: 14 + rightPadding: 14 + text: backend.hostname + placeholderText: "raveos-pc" + color: cTextWhite + placeholderTextColor: "#5b6270" + font.pixelSize: 15 + background: Rectangle { + radius: 7 + color: cInputBg + border.color: hostnameField.activeFocus ? cAccentGreen : cInputBorder + border.width: hostnameField.activeFocus ? 1.5 : 1 + } + onTextEdited: backend.setHostname(text) + } + + Text { + visible: backend.hostnameStatus !== "" + text: backend.hostnameStatus + color: cErrorRed + font.pixelSize: 12 + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 6 + + Text { + text: RaveUsersModule.Translations.passwordLabel + color: "#c7ccd6" + font.pixelSize: 14 + font.weight: Font.Medium + } + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + PasswordField { + Layout.fillWidth: true + reveal: showUserPassword + onRevealClicked: showUserPassword = !showUserPassword + placeholderText: RaveUsersModule.Translations.passwordPlaceholder + onTextEdited: backend.setPassword(text) + } + + PasswordField { + Layout.fillWidth: true + reveal: showUserPassword + onRevealClicked: showUserPassword = !showUserPassword + placeholderText: RaveUsersModule.Translations.passwordConfirmLabel + onTextEdited: backend.setPasswordSecondary(text) + } + } + + Text { + visible: backend.passwordStatus !== "" + text: backend.passwordStatus + color: cErrorRed + font.pixelSize: 12 + } + } + + Rectangle { + Layout.fillWidth: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + height: 1 + color: cCardBorder + opacity: 0.6 + } + + CheckBox { + id: reuseRootCheck + Layout.fillWidth: true + visible: backend.rootReuseVisible + text: RaveUsersModule.Translations.reuseRootLabel + checked: backend.reuseRootPassword + onToggled: backend.setReuseRootPassword(checked) + + indicator: Rectangle { + implicitWidth: 20 + implicitHeight: 20 + x: reuseRootCheck.leftPadding + y: parent.height / 2 - height / 2 + radius: 4 + color: reuseRootCheck.checked ? cAccentGreen : cInputBg + border.color: reuseRootCheck.checked ? cAccentGreen : cInputBorder + border.width: 1 + + Text { + anchors.centerIn: parent + text: "\u2713" + font.pixelSize: 14 + font.bold: true + color: "#0f1713" + visible: reuseRootCheck.checked + } + } + + contentItem: Text { + text: reuseRootCheck.text + color: "#d7dbe2" + font.pixelSize: 14 + verticalAlignment: Text.AlignVCenter + leftPadding: reuseRootCheck.indicator.width + 12 + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 6 + visible: !backend.reuseRootPassword + + Text { + text: RaveUsersModule.Translations.rootPasswordLabel + color: "#c7ccd6" + font.pixelSize: 14 + font.weight: Font.Medium + Layout.topMargin: 4 + } + + RowLayout { + Layout.fillWidth: true + spacing: 12 + + PasswordField { + Layout.fillWidth: true + reveal: showRootPassword + onRevealClicked: showRootPassword = !showRootPassword + placeholderText: RaveUsersModule.Translations.rootPasswordPlaceholder + onTextEdited: backend.setRootPasswordText(text) + } + + PasswordField { + Layout.fillWidth: true + reveal: showRootPassword + onRevealClicked: showRootPassword = !showRootPassword + placeholderText: RaveUsersModule.Translations.rootPasswordConfirmLabel + onTextEdited: backend.setRootPasswordSecondaryText(text) + } + } + + Text { + visible: backend.rootPasswordStatus !== "" + text: backend.rootPasswordStatus + color: cErrorRed + font.pixelSize: 12 + } + } + } + } + } +} diff --git a/raveos-calamares-theme/.DS_Store b/raveos-calamares-theme/.DS_Store Binary files differdeleted file mode 100644 index b5bf013..0000000 --- a/raveos-calamares-theme/.DS_Store +++ /dev/null diff --git a/raveos-calamares-theme/REVIEW.md b/raveos-calamares-theme/REVIEW.md new file mode 100644 index 0000000..b629f31 --- /dev/null +++ b/raveos-calamares-theme/REVIEW.md @@ -0,0 +1,66 @@ +# raveos-calamares-theme — teljes átvizsgálás + +Dátum: 2026-09-18 +Állapot: a korábbi „Missing variables: 3,3,1,u,u,u” hiba már javítva (`42d8e63`). + +## 🔴 Érdemes javítani + +### 1. `shellprocess-final.conf` 34. sor — MESA/__GL env idézőjelezve +A parancs a `/etc/environment`-be **idézőjelekkel együtt** írja a sorokat: + +```text +"MESA_SHADER_CACHE_MAX_SIZE=12G" +"__GL_SHADER_DISK_CACHE_SIZE=12884901888" +``` + +A systemd generátor valószínűleg leszedi a `"`-t, de más olvasók (pl. pam_env) nem +feltétlenül, így a változó értéke `"12G"` lehet. Javasolt csere tiszta `printf`-re: + +```yaml +- "printf 'MESA_SHADER_CACHE_MAX_SIZE=12G\\n__GL_SHADER_DISK_CACHE_SIZE=12884901888\\n' >> ${ROOT}/etc/environment" +``` + +### 2. `services-systemd.conf` bekapcsolja a gdm-et +`- name: gdm, enable: true` — üti az SDDM-default fixet. A modul jelenleg +nincs a sequence-ben (nem fut), de ha beillesztik, a gdm lesz az alap. +Javasolt: `enable: false` vagy a gdm sor törlése. + +### 3. `shellprocess-loader.conf` → btrfs scriptek végén hard fail lehet +- `calamares-normalize-subvolumes.sh` vége: `mount /var/cache/pacman/pkg` +- `calamares-create-snapshots-subvol.sh` vége: `mount /.snapshots` + +Ezek nem `|| true`-sak és a loader konfban nincs `-` prefix; ha a mount elhasal, +a telepítés a legvégén leáll. Javasolt: `|| true` a mount sorokra. + +## 🟡 Kisebb / kockázatos + +### 4. `bootloader.conf` kevert, nem szabványos kulcsok +`bootloader: grub` mellett `efiBootLoader: systemd-boot`, plusz `kernel: +linux-cachyos`, `initramfs: ...`, `grubInstall:` stb. A standard Calamares ezeket +ignorálja, de forkolt modulnál a `systemd-boot` ütheti a grub-ot. Tisztázandó. + +### 5. `.DS_Store` fájlok a csomagban +A PKGBUILD `cp -r "${startdir}/etc/."` a macOS `.DS_Store`-okat is beviszi a +csomagba. Javasolt a `package()`-ben: `find "${pkgdir}" -name .DS_Store -delete`. + +### 6. `netinstall-drivers.yaml` elírás +`exlusive: false` → helyesen `exclusive`. (Most nem fut, mert a netinstall +ki van kommentezve a sequence-ből.) + +### 7. `shellprocess-branding.conf` — nincs ignore-failure prefix +A live-on futó `pacman-key --recv-key` / `--lsign-key` sorokon nincs `-` prefix +(ellentétben a `shellprocess-before`-ral). Ha a keyserver nem elérhető, az +install leállhat. Javasolt `-` prefix ide is. + +### 8. `welcome.conf` internetCheckUrl +`http://example.com` megbízhatatlan; érdemes valós, stabil URL-re cserélni. + +## 🟢 Rendben van + +- Mind a 7 `.sh` script `bash -n` tiszta; `raveos-chown-homes.sh` jó. +- `settings.conf` sequence konzisztens (netinstall szándékosan kikapcsolva; + a csomagokat a desktopselect hozza). +- `displaymanager.conf` SDDM default; `shellprocess-enableservices.conf` + SDDM enable + gdm disable — konzisztens. +- users, locale, keyboard, grubcfg, mount, partition, preservefiles, + finished konfok rendben. diff --git a/raveos-calamares-theme/etc/.DS_Store b/raveos-calamares-theme/etc/.DS_Store Binary files differdeleted file mode 100644 index e3a60e9..0000000 --- a/raveos-calamares-theme/etc/.DS_Store +++ /dev/null diff --git a/raveos-calamares-theme/etc/calamares/.DS_Store b/raveos-calamares-theme/etc/calamares/.DS_Store Binary files differdeleted file mode 100644 index 0f5c1e6..0000000 --- a/raveos-calamares-theme/etc/calamares/.DS_Store +++ /dev/null diff --git a/raveos-calamares-theme/etc/calamares/branding/.DS_Store b/raveos-calamares-theme/etc/calamares/branding/.DS_Store Binary files differdeleted file mode 100644 index 54ecfe9..0000000 --- a/raveos-calamares-theme/etc/calamares/branding/.DS_Store +++ /dev/null diff --git a/raveos-calamares-theme/etc/calamares/branding/raveos/.DS_Store b/raveos-calamares-theme/etc/calamares/branding/raveos/.DS_Store Binary files differdeleted file mode 100644 index f2e5f54..0000000 --- a/raveos-calamares-theme/etc/calamares/branding/raveos/.DS_Store +++ /dev/null diff --git a/raveos-calamares-theme/etc/calamares/settings.conf b/raveos-calamares-theme/etc/calamares/settings.conf index f40f8ab..9780d3c 100755 --- a/raveos-calamares-theme/etc/calamares/settings.conf +++ b/raveos-calamares-theme/etc/calamares/settings.conf @@ -154,7 +154,7 @@ sequence: - locale - keyboard - partition - - users + - raveusers - desktopselect # - netinstall@apps # - tracking @@ -176,7 +176,7 @@ sequence: - initcpio - bootloader - removeuser - - users + - raveusers - shellprocess@copybrave - networkcfg - hwclock diff --git a/raveos-updater/PKGBUILD b/raveos-updater/PKGBUILD new file mode 100644 index 0000000..6bb6622 --- /dev/null +++ b/raveos-updater/PKGBUILD @@ -0,0 +1,28 @@ +# Maintainer: nippy <nippy@tuta.io> + +pkgname=raveos-updater +pkgver=1.0.0 +pkgrel=1 +pkgdesc="RaveOS update checker background service and tray indicator" +arch=('any') +url="https://git.rp1.hu/RaveOS" +license=('GPL') +depends=('python' 'python-pyqt6' 'pacman') +install="${pkgname}.install" +source=() +sha256sums=() + +package() { + install -Dm755 "${startdir}/check.py" \ + "${pkgdir}/usr/lib/raveos-updater/check.py" + install -Dm755 "${startdir}/raveos-updater" \ + "${pkgdir}/usr/bin/raveos-updater" + install -Dm644 "${startdir}/raveos-updater.service" \ + "${pkgdir}/usr/lib/systemd/system/raveos-updater.service" + install -Dm644 "${startdir}/raveos-updater.timer" \ + "${pkgdir}/usr/lib/systemd/system/raveos-updater.timer" + install -Dm644 "${startdir}/raveos-updater.hook" \ + "${pkgdir}/usr/share/libalpm/hooks/raveos-updater.hook" + install -Dm644 "${startdir}/raveos-updater.desktop" \ + "${pkgdir}/etc/xdg/autostart/raveos-updater.desktop" +} diff --git a/raveos-updater/check.py b/raveos-updater/check.py new file mode 100644 index 0000000..79a0ba3 --- /dev/null +++ b/raveos-updater/check.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""RaveOS frissítés-ellenőrző. + +A raveos-core-repo szinkron adatbázisából kiolvassa a RaveOS saját +csomagjainak legfrissebb verzióját, összeveti a telepített verziókkal, +és a /var/lib/raveos-updater/status.json fájlba írja az eredményt. + +Használat: + check.py # pacman -Sy után ellenőriz (root; a systemd timer hívja) + check.py --no-sync # nem szinkronizál, a meglévő DB-t használja (pacman hook) +""" + +import json +import os +import subprocess +import sys +from datetime import datetime + +REPO = "raveos-core-repo" +STATUS_DIR = "/var/lib/raveos-updater" +STATUS_FILE = os.path.join(STATUS_DIR, "status.json") + + +def run(args): + return subprocess.run(args, capture_output=True, text=True) + + +def main(): + sync = "--no-sync" not in sys.argv[1:] + error = None + + if sync: + r = run(["pacman", "-Sy", "--noconfirm"]) + if r.returncode != 0: + error = "sync failed" + + installed = {} + repo = {} + + try: + for line in run(["pacman", "-Q"]).stdout.splitlines(): + name, _, version = line.partition(" ") + if version: + installed[name] = version + + for line in run(["pacman", "-Sl", REPO]).stdout.splitlines(): + parts = line.split() + if len(parts) >= 3: + repo[parts[1]] = parts[2] + except FileNotFoundError: + error = "pacman not found" + + updates = [] + for name, newver in sorted(repo.items()): + oldver = installed.get(name) + if oldver is None: + continue + r = run(["vercmp", newver, oldver]) + try: + newer = int(r.stdout.strip()) > 0 + except (ValueError, AttributeError): + continue + if newer: + updates.append({"name": name, "old": oldver, "new": newver}) + + status = { + "count": len(updates), + "checked": datetime.now().astimezone().isoformat(timespec="seconds"), + "updates": updates, + } + if error: + status["error"] = error + + os.makedirs(STATUS_DIR, exist_ok=True) + tmp = STATUS_FILE + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(status, f, ensure_ascii=False, indent=2) + os.replace(tmp, STATUS_FILE) + os.chmod(STATUS_FILE, 0o644) + + +if __name__ == "__main__": + main() diff --git a/raveos-updater/raveos-updater b/raveos-updater/raveos-updater new file mode 100644 index 0000000..769da64 --- /dev/null +++ b/raveos-updater/raveos-updater @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""RaveOS Update Indicator — tálca-jelző. + +Önálló (DE-független) tálca-alkalmazás. A /var/lib/raveos-updater/status.json +fájlt olvassa, amit a háttérszolgáltatás (check.py + systemd timer) állít elő. +A jobb felső tálcán egy ikon mutatja, ha van elérhető RaveOS frissítés. + +Első verzió: GNOME (Qt6 SNI tálca az AppIndicator kiegészítővel). +Később Plasma/Hyprland/COSMIC is ugyanezt a status.json-t használja. +""" + +import json +import os +import shlex +import shutil +import subprocess +import sys + +from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtGui import QAction, QBrush, QColor, QFont, QIcon, QPainter, QPen, QPixmap +from PyQt6.QtWidgets import QApplication, QMenu, QSystemTrayIcon + +STATUS_FILE = "/var/lib/raveos-updater/status.json" +CHECK_CMD = "/usr/lib/raveos-updater/check.py" +APP_NAME = "RaveOS Update Indicator" + + +def read_status(): + try: + with open(STATUS_FILE, encoding="utf-8") as f: + return json.load(f) + except Exception: + return None + + +def make_icon(count): + pm = QPixmap(64, 64) + pm.fill(Qt.GlobalColor.transparent) + p = QPainter(pm) + p.setRenderHint(QPainter.RenderHint.Antialiasing) + + if count: + p.setBrush(QBrush(QColor("#23e06b"))) + p.setPen(QPen(QColor("#12833c"), 3)) + else: + p.setBrush(QBrush(QColor("#5a5a5a"))) + p.setPen(QPen(QColor("#3a3a3a"), 3)) + p.drawEllipse(6, 6, 52, 52) + + p.setPen(QColor("#ffffff")) + if count: + font = QFont() + font.setBold(True) + font.setPixelSize(30) + p.setFont(font) + p.drawText(pm.rect(), Qt.AlignmentFlag.AlignCenter, str(count) if count < 100 else "99") + else: + pen = QPen(QColor("#ffffff"), 7) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin) + p.setPen(pen) + p.drawLine(18, 33, 28, 43) + p.drawLine(28, 43, 47, 20) + p.end() + return QIcon(pm) + + +def spawn_terminal(cmd): + candidates = [ + ["kgx", "-e", "bash", "-c", cmd], + ["gnome-terminal", "--", "bash", "-c", cmd], + ["konsole", "-e", "bash", "-c", cmd], + ["xterm", "-e", "bash", "-c", cmd], + ] + for c in candidates: + if shutil.which(c[0]): + subprocess.Popen(c, start_new_session=True) + return True + return False + + +class Updater: + def __init__(self, app): + self.app = app + self.tray = QSystemTrayIcon() + self.tray.setToolTip(APP_NAME) + self.menu = QMenu() + self.tray.setContextMenu(self.menu) + self.refresh() + self.tray.show() + # Qt6 SNI: a tálca-ikon regisztrációjához legalább egy esemény-hurok + # iteráció kell, különben GNOME-on nem jelenik meg az ikon. + self.app.processEvents() + + self.timer = QTimer() + self.timer.timeout.connect(self.refresh) + self.timer.start(60_000) + + def refresh(self): + st = read_status() + self.menu.clear() + + if st is None: + self.tray.setIcon(make_icon(0)) + self._add("RaveOS állapot ismeretlen", enabled=False) + self._add("(az ellenőrzés még nem futott le)", enabled=False) + elif st.get("error"): + self.tray.setIcon(make_icon(0)) + self._add("RaveOS ellenőrzés sikertelen", enabled=False) + self._add(st["error"], enabled=False) + else: + count = st.get("count", 0) + self.tray.setIcon(make_icon(count)) + if count: + self._add(f"{count} RaveOS frissítés elérhető", enabled=False) + self.tray.setToolTip(f"{APP_NAME}: {count} frissítés elérhető") + self.menu.addSeparator() + for u in st.get("updates", []): + self._add(f"{u['name']} {u['old']} → {u['new']}", enabled=False) + else: + self._add("RaveOS naprakész", enabled=False) + self.tray.setToolTip(f"{APP_NAME}: naprakész") + checked = st.get("checked", "") + if checked: + self._add("Ellenőrizve: " + checked.replace("T", " ")[:16], enabled=False) + + self.menu.addSeparator() + self._add("Frissítés most", self._update_now) + self._add("Ellenőrzés most", self._check_now) + self._add("Kilépés", self.app.quit) + + def _add(self, text, callback=None, enabled=True): + act = QAction(text, self.menu) + act.setEnabled(enabled) + if callback: + act.triggered.connect(callback) + self.menu.addAction(act) + return act + + def _update_now(self): + spawn_terminal("rave upgrade; exec bash") + + def _check_now(self): + if shutil.which("pkexec"): + subprocess.Popen(["pkexec", CHECK_CMD], start_new_session=True) + else: + subprocess.Popen(["sudo", CHECK_CMD], start_new_session=True) + + +def main(): + app = QApplication(sys.argv) + app.setApplicationName(APP_NAME) + app.setQuitOnLastWindowClosed(False) + + if not QSystemTrayIcon.isSystemTrayAvailable(): + print("RaveOS Update Indicator: nincs elérhető tálca (SNI) a jelenlegi környezetben.", + file=sys.stderr) + + Updater(app) + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/raveos-updater/raveos-updater.desktop b/raveos-updater/raveos-updater.desktop new file mode 100644 index 0000000..501f8c7 --- /dev/null +++ b/raveos-updater/raveos-updater.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Name=RaveOS Update Indicator +Name[hu]=RaveOS Frissítésjelző +Comment=Shows when RaveOS package updates are available +Comment[hu]=Jelzi, ha elérhető RaveOS frissítés +Exec=/usr/bin/raveos-updater +Icon=raveos-updater +Terminal=false +NoDisplay=true +X-GNOME-Autostart-enabled=true +StartupNotify=false diff --git a/raveos-updater/raveos-updater.hook b/raveos-updater/raveos-updater.hook new file mode 100644 index 0000000..ebc1583 --- /dev/null +++ b/raveos-updater/raveos-updater.hook @@ -0,0 +1,11 @@ +[Trigger] +Operation = Install +Operation = Upgrade +Operation = Remove +Type = Package +Target = * + +[Action] +Description = Refreshing RaveOS update status +When = PostTransaction +Exec = /usr/lib/raveos-updater/check.py --no-sync diff --git a/raveos-updater/raveos-updater.install b/raveos-updater/raveos-updater.install new file mode 100644 index 0000000..f2993fd --- /dev/null +++ b/raveos-updater/raveos-updater.install @@ -0,0 +1,20 @@ +_enable() { + if command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload >/dev/null 2>&1 || true + systemctl enable --now raveos-updater.timer >/dev/null 2>&1 || true + fi +} + +post_install() { + _enable +} + +post_upgrade() { + _enable +} + +pre_remove() { + if command -v systemctl >/dev/null 2>&1; then + systemctl disable --now raveos-updater.timer >/dev/null 2>&1 || true + fi +} diff --git a/raveos-updater/raveos-updater.service b/raveos-updater/raveos-updater.service new file mode 100644 index 0000000..118c71e --- /dev/null +++ b/raveos-updater/raveos-updater.service @@ -0,0 +1,6 @@ +[Unit] +Description=RaveOS update check service + +[Service] +Type=oneshot +ExecStart=/usr/lib/raveos-updater/check.py diff --git a/raveos-updater/raveos-updater.timer b/raveos-updater/raveos-updater.timer new file mode 100644 index 0000000..9ab66a9 --- /dev/null +++ b/raveos-updater/raveos-updater.timer @@ -0,0 +1,10 @@ +[Unit] +Description=RaveOS update check timer + +[Timer] +OnBootSec=5min +OnUnitActiveSec=2h +Persistent=true + +[Install] +WantedBy=timers.target |