1 Commits

Author SHA1 Message Date
674bb81358 initial packagechooser loaderqueue implement 2024-01-06 00:09:31 +01:00
122 changed files with 5088 additions and 2925 deletions

View File

@@ -56,29 +56,6 @@ project(calamares-extensions
set( CMAKE_CXX_STANDARD 17 )
set( CMAKE_CXX_STANDARD_REQUIRED ON )
if(WITH_QT6)
set(kfname "KF6")
set(KF_VERSION 5.240) # KDE Neon weirdness
else()
message(STATUS "Building Calamares with Qt5")
set(kfname "KF5")
set(KF_VERSION 5.78)
# API that was deprecated before Qt 5.15 causes a compile error
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050f00)
endif()
include( FeatureSummary )
find_package(${kfname}CoreAddons ${KF_VERSION} QUIET)
set_package_properties(
${kfname}CoreAddons
PROPERTIES
TYPE REQUIRED
DESCRIPTION "KDE Framework CoreAddons"
URL "https://api.kde.org/frameworks/"
PURPOSE "Essential Framework for AboutData and Macros"
)
# On developer's machine, the user package registry breaks
# consumers by loading the developer's config from a build
# directory (which doesn't have the rest of the config
@@ -119,19 +96,18 @@ include( CTest )
# Typically you would use only one branding, since that's
# the (single) branding for your distro.
#
# calamares_add_branding_subdirectory( branding/default NAME default )
# calamares_add_branding_subdirectory( branding/default-mobile NAME default-mobile )
# calamares_add_branding_subdirectory( branding/fancy NAME fancy )
calamares_add_branding_subdirectory( branding/default NAME default )
calamares_add_branding_subdirectory( branding/default-mobile NAME default-mobile )
calamares_add_branding_subdirectory( branding/fancy NAME fancy )
# This one has files in subdirectories
# calamares_add_branding_subdirectory( branding/samegame NAME samegame SUBDIRECTORIES img )
calamares_add_branding_subdirectory( branding/samegame NAME samegame SUBDIRECTORIES img )
# KaOS branding, with translations, note we can *NAME* something
# different from the source directory it lives in; this will be installed
# to a directory called *NAME* though -- and the `branding.desc` must
# have a *componentName* that matches this *NAME*.
# calamares_add_branding_subdirectory( branding/kaos_branding NAME kaos )
calamares_add_branding_subdirectory( branding/artix NAME artix )
calamares_add_branding_subdirectory( branding/kaos_branding NAME kaos )
### MODULES
#
@@ -139,22 +115,13 @@ calamares_add_branding_subdirectory( branding/artix NAME artix )
#
set(LIST_SKIPPED_MODULES "")
# calamares_add_module_subdirectory( modules/freebsddisk LIST_SKIPPED_MODULES ) # C++ viewmodule
# calamares_add_module_subdirectory( modules/mobile LIST_SKIPPED_MODULES )
# calamares_add_module_subdirectory( modules/os-freebsd LIST_SKIPPED_MODULES )
# calamares_add_module_subdirectory( modules/os-nixos LIST_SKIPPED_MODULES )
# calamares_add_module_subdirectory( modules/refind LIST_SKIPPED_MODULES )
# calamares_add_module_subdirectory( modules/slowpython LIST_SKIPPED_MODULES ) # Python job
# calamares_add_module_subdirectory( modules/unpackfsc LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/basestrap LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/services-artix LIST_SKIPPED_MODULES )
# calamares_add_module_subdirectory( modules/services-runit LIST_SKIPPED_MODULES )
# calamares_add_module_subdirectory( modules/services-dinit LIST_SKIPPED_MODULES )
# calamares_add_module_subdirectory( modules/services-s6 LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/postcfg LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/packagechooser LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/packagechooserq LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/freebsddisk LIST_SKIPPED_MODULES ) # C++ viewmodule
calamares_add_module_subdirectory( modules/mobile LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/os-freebsd LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/os-nixos LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/refind LIST_SKIPPED_MODULES )
calamares_add_module_subdirectory( modules/slowpython LIST_SKIPPED_MODULES ) # Python job
calamares_add_module_subdirectory( modules/unpackfsc LIST_SKIPPED_MODULES )
message(STATUS "Calamares extensions ${CALAMARES_EXTENSIONS_VERSION} for Calamares version ${Calamares_VERSION}")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1,239 +0,0 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
#
# Product branding information. This influences some global
# user-visible aspects of Calamares, such as the product
# name, window behavior, and the slideshow during installation.
#
# Additional styling can be done using the stylesheet.qss
# file, also in the branding directory.
---
componentName: artix
### WELCOME / OVERALL WORDING
#
# These settings affect some overall phrasing and looks,
# which are most visible in the welcome page.
# This selects between different welcome texts. When false, uses
# the traditional "Welcome to the %1 installer.", and when true,
# uses "Welcome to the Calamares installer for %1." This allows
# to distinguish this installer from other installers for the
# same distribution.
welcomeStyleCalamares: false
# Should the welcome image (productWelcome, below) be scaled
# up beyond its natural size? If false, the image does not grow
# with the window but remains the same size throughout (this
# may have surprising effects on HiDPI monitors).
welcomeExpandingLogo: true
### WINDOW CONFIGURATION
#
# The settings here affect the placement of the Calamares
# window through hints to the window manager and initial
# sizing of the Calamares window.
# Size and expansion policy for Calamares.
# - "normal" or unset, expand as needed, use *windowSize*
# - "fullscreen", start as large as possible, ignore *windowSize*
# - "noexpand", don't expand automatically, use *windowSize*
windowExpanding: normal
# Size of Calamares window, expressed as w,h. Both w and h
# may be either pixels (suffix px) or font-units (suffix em).
# e.g. "800px,600px"
# "60em,480px"
# This setting is ignored if "fullscreen" is selected for
# *windowExpanding*, above. If not set, use constants defined
# in CalamaresUtilsGui, 800x520.
windowSize: 800px,520px
# Placement of Calamares window. Either "center" or "free".
# Whether "center" actually works does depend on the window
# manager in use (and only makes sense if you're not using
# *windowExpanding* set to "fullscreen").
windowPlacement: center
### PANELS CONFIGURATION
#
# Calamares has a main content area, and two panels (navigation
# and progress / sidebar). The panels can be controlled individually,
# or switched off. If both panels are switched off, the layout of
# the main content area loses its margins, on the assumption that
# you're doing something special.
# Kind of sidebar (panel on the left, showing progress).
# - "widget" or unset, use traditional sidebar (logo, items)
# - "none", hide it entirely
# - "qml", use calamares-sidebar.qml from branding folder
# In addition, you **may** specify a side, separated by a comma,
# from the kind. Valid sides are:
# - "left" (if not specified, uses this)
# - "right"
# - "top"
# - "bottom"
# For instance, "widget,right" is valid; so is "qml", which defaults
# to putting the sidebar on the left. Also valid is "qml,top".
# While "widget,top" is valid, the widgets code is **not** flexible
# and results will be terrible.
sidebar: widget
# Kind of navigation (button panel on the bottom).
# - "widget" or unset, use traditional navigation
# - "none", hide it entirely
# - "qml", use calamares-navigation.qml from branding folder
# In addition, you **may** specify a side, separated by a comma,
# from the kind. The same sides are valid as for *sidebar*,
# except the default is *bottom*.
navigation: widget
### STRINGS, IMAGES AND COLORS
#
# This section contains the "branding proper" of names
# and images, rather than global-look settings.
# These are strings shown to the user in the user interface.
# There is no provision for translating them -- since they
# are names, the string is included as-is.
#
# The four Url strings are the Urls used by the buttons in
# the welcome screen, and are not shown to the user. Clicking
# on the "Support" button, for instance, opens the link supportUrl.
# If a Url is empty, the corresponding button is not shown.
#
# bootloaderEntryName is how this installation / distro is named
# in the boot loader (e.g. in the GRUB menu).
#
# These strings support substitution from /etc/os-release
# if KDE Frameworks 5.58 are available at build-time. When
# enabled, ${varname} is replaced by the equivalent value
# from os-release. All the supported var-names are in all-caps,
# and are listed on the FreeDesktop.org site,
# https://www.freedesktop.org/software/systemd/man/os-release.html
# Note that ANSI_COLOR and CPE_NAME don't make sense here, and
# are not supported (the rest are). Remember to quote the string
# if it contains substitutions, or you'll get YAML exceptions.
#
# The *Url* entries are used on the welcome page, and they
# are visible as buttons there if the corresponding *show* keys
# are set to "true" (they can also be overridden).
strings:
productName: Artix Linux
shortProductName: Artix
version: rolling
shortVersion: rolling
versionedName: Artix Linux "rolling"
shortVersionedName: Artix rolling
bootloaderEntryName: Artix
productUrl: https://www.artixlinux.org/
supportUrl: https://github.com/calamares/calamares/issues
knownIssuesUrl: https://calamares.io/about/
releaseNotesUrl: https://calamares.io/about/
# donateUrl: https://kde.org/community/donations/index.php
# These images are loaded from the branding module directory.
#
# productBanner is an optional image, which if present, will be shown
# on the welcome page of the application, above the welcome text.
# It is intended to have a width much greater than height.
# It is displayed at 64px height (also on HiDPI).
# Recommended size is 64px tall, and up to 460px wide.
# productIcon is used as the window icon, and will (usually) be used
# by the window manager to represent the application. This image
# should be square, and may be displayed by the window manager
# as small as 16x16 (but possibly larger).
# productLogo is used as the logo at the top of the left-hand column
# which shows the steps to be taken. The image should be square,
# and is displayed at 80x80 pixels (also on HiDPI).
# productWallpaper is an optional image, which if present, will replace
# the normal solid background on every page of the application.
# It can be any size and proportion,
# and will be tiled to fit the entire window.
# For a non-tiled wallpaper, the size should be the same as
# the overall window, see *windowSize* above (800x520).
# productWelcome is shown on the welcome page of the application in
# the middle of the window, below the welcome text. It can be
# any size and proportion, and will be scaled to fit inside
# the window. Use `welcomeExpandingLogo` to make it non-scaled.
# Recommended size is 320x150.
#
# These filenames can also use substitutions from os-release (see above).
images:
productBanner: "banner.png"
productIcon: "logo.png"
productLogo: "logo.png"
# productWallpaper: "wallpaper.png"
productWelcome: "languages.png"
# Colors for text and background components.
#
# - SidebarBackground is the background of the sidebar
# - SidebarText is the (foreground) text color
# - SidebarBackgroundCurrent sets the background of the current step.
# Optional, and defaults to the application palette.
# - SidebarTextCurrent is the text color of the current step.
#
# These colors can **also** be set through the stylesheet, if the
# branding component also ships a stylesheet.qss. Then they are
# the corresponding CSS attributes of #sidebarApp.
style:
SidebarBackground: "#292F34"
SidebarText: "#FFFFFF"
SidebarTextCurrent: "#292F34"
SidebarBackgroundCurrent: "#16a3f5"
### SLIDESHOW
#
# The slideshow is displayed during execution steps (e.g. when the
# installer is actually writing to disk and doing other slow things).
# The slideshow can be a QML file (recommended) which can display
# arbitrary things -- text, images, animations, or even play a game --
# during the execution step. The QML **is** abruptly stopped when the
# execution step is done, though, so maybe a game isn't a great idea.
#
# The slideshow can also be a sequence of images (not recommended unless
# you don't want QML at all in your Calamares). The images are displayed
# at a rate of 1 every 2 seconds during the execution step.
#
# To configure a QML file, list a single filename:
# slideshow: "show.qml"
# To configure images, like the filenames (here, as an inline list):
# slideshow: [ "/etc/calamares/slideshow/0.png", "/etc/logo.png" ]
slideshow: "show.qml"
# There are two available APIs for a QML slideshow:
# - 1 (the default) loads the entire slideshow when the installation-
# slideshow page is shown and starts the QML then. The QML
# is never stopped (after installation is done, times etc.
# continue to fire).
# - 2 loads the slideshow on startup and calls onActivate() and
# onLeave() in the root object. After the installation is done,
# the show is stopped (first by calling onLeave(), then destroying
# the QML components).
#
# An image slideshow does not need to have the API defined.
slideshowAPI: 2
# These options are to customize online uploading of logs to pastebins:
# - type : Defines the kind of pastebin service to be used. Currently
# it accepts two values:
# - none : disables the pastebin functionality
# - fiche : use fiche pastebin server
# - url : Defines the address of pastebin service to be used.
# Takes string as input. Important bits are the host and port,
# the scheme is not used.
# - sizeLimit : Defines maximum size limit (in KiB) of log file to be pasted.
# The option must be set, to have the log option work.
# Takes integer as input. If < 0, no limit will be forced,
# else only last (approximately) 'n' KiB of log file will be pasted.
# Please note that upload size may be slightly over the limit (due
# to last minute logging), so provide a suitable value.
uploadServer :
type : "fiche"
url : "http://termbin.com:9999"
sizeLimit : -1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2015 Teo Mrnjavac <teo@kde.org>
SPDX-License-Identifier: GPL-3.0-or-later

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 873 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -1,178 +0,0 @@
/* === This file is part of Calamares - <http://github.com/calamares> ===
*
* Copyright 2015, Teo Mrnjavac <teo@kde.org>
* Copyright 2016, Luca Giambonini <almack@chakralinux.org>
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Calamares is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.0;
import calamares.slideshow 1.0;
/* Tested with slide images of 1800x1200 and 1632x1248 pixels */
Presentation
{
id: presentation
Timer {
interval: 10000
running: true
repeat: true
onTriggered: presentation.goToNextSlide()
}
Slide {
Image {
id: pic0
source: "thanks.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic00
source: "artix.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic02
source: "tools.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic03
source: "desktops.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic04
source: "productivity.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic06
source: "browsers.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic07
source: "office.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic08
source: "multimedia.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic09
source: "web.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic11
source: "packages1.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic13
source: "packages2.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic15
source: "customise.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
Slide {
Image {
id: pic16
source: "rolling.png"
width: parent.width * 1.12; height: parent.height * 1.5
fillMode: Image.Stretch
anchors.centerIn: parent
anchors.verticalCenterOffset: -20
}
}
}

View File

@@ -1,96 +0,0 @@
/*
* SPDX-FileCopyrightText: no
* SPDX-License-Identifier: CC0-1.0
*/
/*
A branding component can ship a stylesheet (like this one)
which is applied to parts of the Calamares user-interface.
In principle, all parts can be styled through CSS.
Missing parts should be filed as issues.
The IDs are based on the object names in the C++ code.
You can use the Debug Dialog to find out object names:
- Open the debug dialog
- Choose tab *Tools*
- Click *Widget Tree* button
The list of object names is printed in the log.
Documentation for styling Qt Widgets through a stylesheet
can be found at
https://doc.qt.io/qt-5/stylesheet-examples.html
https://doc.qt.io/qt-5/stylesheet-reference.html
In Calamares, styling widget classes is supported (e.g.
using `QComboBox` as a selector).
This example stylesheet has all the actual styling commented out.
The examples are not exhaustive.
*/
/*** Generic Widgets.
*
* You can style **all** widgets of a given class by selecting
* the class name. Some widgets have specialized sub-selectors.
*/
/*
QPushButton { background-color: green; }
*/
/*** Main application window.
*
* The main application window has the sidebar, which in turn
* contains a logo and a list of items -- note that the list
* can **not** be styled, since it has its own custom C++
* delegate code.
*/
/*
#mainApp { }
#sidebarApp { }
#logoApp { }
*/
/*** Welcome module.
*
* There are plenty of parts, but the buttons are the most interesting
* ones (donate, release notes, ...). The little icon image can be
* styled through *qproperty-icon*, which is a little obscure.
* URLs can reference the QRC paths of the Calamares application
* or loaded via plugins or within the filesystem. There is no
* comprehensive list of available icons, though.
*/
/*
QPushButton#aboutButton { qproperty-icon: url(:/data/images/release.svg); }
#donateButton,
#supportButton,
#releaseNotesButton,
#knownIssuesButton { qproperty-icon: url(:/data/images/help.svg); }
*/
/*** Partitioning module.
*
* Many moving parts, which you will need to experiment with.
*/
/*
#bootInfoIcon { }
#bootInfoLable { }
#deviceInfoIcon { }
#defineInfoLabel { }
#scrollAreaWidgetContents { }
#partitionBarView { }
*/
/*** Licensing module.
*
* The licensing module paints individual widgets for each of
* the licenses. The item can be collapsed or expanded.
*/
/*
#licenseItem { }
#licenseItemFullText { }
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -1,75 +0,0 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
#
# The configuration for the package manager starts with the
# *backend* key, which picks one of the backends to use.
# In `main.py` there is a base class `PackageManager`.
# Implementations must subclass that and set a (class-level)
# property *backend* to the name of the backend (e.g. "dummy").
# That property is used to match against the *backend* key here.
#
# You will have to add such a class for your package manager.
# It is fairly simple Python code. The API is described in the
# abstract methods in class `PackageManager`. Mostly, the only
# trick is to figure out the correct commands to use, and in particular,
# whether additional switches are required or not. Some package managers
# have more installer-friendly defaults than others, e.g., DNF requires
# passing --disablerepo=* -C to allow removing packages without Internet
# connectivity, and it also returns an error exit code if the package did
# not exist to begin with.
---
#
# Which package manager to use, options are:
# - pacman - Pacman
#
# Not actually a package manager, but suitable for testing:
# - dummy - Dummy manager, only logs
#
backend: dummy
# pacman specific options
#
# *num_retries* should be a positive integer which specifies the
# number of times the call to pacman will be retried in the event of a
# failure. If it is missing, it will be set to 0.
#
# *disable_download_timeout* is a boolean that, when true, includes
# the flag --disable-download-timeout on calls to pacman. When missing,
# false is assumed.
#
# *needed_only* is a boolean that includes the pacman argument --needed
# when set to true. If missing, false is assumed.
# *handle_keyrings* is a boolean that includes initializing and populating keyrings
# when set to true. If missing, false is assumed.
pacman:
num_retries: 0
disable_download_timeout: false
needed_only: false
handle_keyrings: false
copy_pacconf: false
requirements:
- name: /etc
mode: "0o755"
- name: /var
mode: "0o755"
- name: /var/cache
mode: "0o755"
- name: /var/cache/pacman
mode: "0o755"
- name: /var/cache/pacman/pkg
mode: "0o755"
- name: /var/lib
mode: "0o755"
- name: /var/lib/pacman
mode: "0o755"
keyrings:
- artix
# the artix base package allows selection of the init system tied to elogind
# this option is artix specific
# base_init: elogind
operations:
- install:
- base

View File

@@ -1,552 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2014 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com>
# SPDX-FileCopyrightText: 2015-2017 Teo Mrnjavac <teo@kde.org>
# SPDX-FileCopyrightText: 2016-2017 Kyle Robbertze <kyle@aims.ac.za>
# SPDX-FileCopyrightText: 2017 Alf Gaida <agaida@siduction.org>
# SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
# SPDX-FileCopyrightText: 2018 Philip Müller <philm@manjaro.org>
# SPDX-FileCopyrightText: 2023 Artoo <artoo@artixlinux.org>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Calamares is Free Software: see the License-Identifier above.
#
import abc
from string import Template
import os, shutil, subprocess, sys
import libcalamares
from libcalamares.utils import host_env_process_output, target_env_process_output
from libcalamares.utils import gettext_path, gettext_languages
from os.path import join
import gettext
_translation = gettext.translation("calamares-python",
localedir=gettext_path(),
languages=gettext_languages(),
fallback=True)
_ = _translation.gettext
_n = _translation.ngettext
total_packages = 0 # For the entire job
completed_packages = 0 # Done so far for this job
group_packages = 0 # One group of packages from an -install or -remove entry
# A PM object may set this to a string (take care of translations!)
# to override the string produced by pretty_status_message()
custom_status_message = None
INSTALL = object()
REMOVE = object()
mode_packages = None # Changes to INSTALL or REMOVE
def _change_mode(mode):
global mode_packages
mode_packages = mode
libcalamares.job.setprogress(completed_packages * 1.0 / total_packages)
def pretty_name():
return _("Install packages.")
def pretty_status_message():
if custom_status_message is not None:
return custom_status_message
if not group_packages:
if (total_packages > 0):
# Outside the context of an operation
s = _("Processing packages (%(count)d / %(total)d)")
else:
s = _("Install packages.")
elif mode_packages is INSTALL:
s = _n("Installing one package.",
"Installing %(num)d packages.", group_packages)
elif mode_packages is REMOVE:
s = _n("Removing one package.",
"Removing %(num)d packages.", group_packages)
else:
# No mode, generic description
s = _("Install packages.")
return s % {"num": group_packages,
"count": completed_packages,
"total": total_packages}
class PackageManager(metaclass=abc.ABCMeta):
"""
Package manager base class. A subclass implements package management
for a specific backend, and must have a class property `backend`
with the string identifier for that backend.
Subclasses are collected below to populate the list of possible
backends.
"""
backend = None
@abc.abstractmethod
def install(self, pkgs, from_local=False):
"""
Install a list of packages (named) into the system.
Although this handles lists, in practice it is called
with one package at a time.
@param pkgs: list[str]
list of package names
@param from_local: bool
if True, then these are local packages (on disk) and the
pkgs names are paths.
"""
pass
@abc.abstractmethod
def remove(self, pkgs):
"""
Removes packages.
@param pkgs: list[str]
list of package names
"""
pass
def run(self, script):
if script != "":
host_env_process_output(script.split(" "))
def install_package(self, packagedata, from_local=False):
"""
Install a package from a single entry in the install list.
This can be either a single package name, or an object
with pre- and post-scripts. If @p packagedata is a dict,
it is assumed to follow the documented structure.
@param packagedata: str|dict
@param from_local: bool
see install.from_local
"""
if isinstance(packagedata, str):
self.install([packagedata], from_local=from_local)
else:
self.run(packagedata["pre-script"])
self.install([packagedata["package"]], from_local=from_local)
self.run(packagedata["post-script"])
def remove_package(self, packagedata):
"""
Remove a package from a single entry in the remove list.
This can be either a single package name, or an object
with pre- and post-scripts. If @p packagedata is a dict,
it is assumed to follow the documented structure.
@param packagedata: str|dict
"""
if isinstance(packagedata, str):
self.remove([packagedata])
else:
self.run(packagedata["pre-script"])
self.remove([packagedata["package"]])
self.run(packagedata["post-script"])
def operation_install(self, package_list, from_local=False):
"""
Installs the list of packages named in @p package_list .
These can be strings -- plain package names -- or
structures (with a pre- and post-install step).
This operation is called for "critical" packages,
which are expected to succeed, or fail, all together.
However, if there are packages with pre- or post-scripts,
then packages are installed one-by-one instead.
NOTE: package managers may reimplement this method
NOTE: exceptions are expected to leave this method, to indicate
failure of the installation.
"""
if all([isinstance(x, str) for x in package_list]):
self.install(package_list, from_local=from_local)
else:
for package in package_list:
self.install_package(package, from_local=from_local)
def operation_try_install(self, package_list):
"""
Installs the list of packages named in @p package_list .
These can be strings -- plain package names -- or
structures (with a pre- and post-install step).
This operation is called for "non-critical" packages,
which can succeed or fail without affecting the overall installation.
Packages are installed one-by-one to support package managers
that do not have a "install as much as you can" mode.
NOTE: package managers may reimplement this method
NOTE: no package-installation exceptions should be raised
"""
# we make a separate package manager call for each package so a
# single failing package won't stop all of them
for package in package_list:
try:
self.install_package(package)
except subprocess.CalledProcessError:
libcalamares.utils.warning("Could not install package %s" % package)
def operation_remove(self, package_list):
"""
Removes the list of packages named in @p package_list .
These can be strings -- plain package names -- or
structures (with a pre- and post-install step).
This operation is called for "critical" packages, which are
expected to succeed or fail all together.
However, if there are packages with pre- or post-scripts,
then packages are removed one-by-one instead.
NOTE: package managers may reimplement this method
NOTE: exceptions should be raised to indicate failure
"""
if all([isinstance(x, str) for x in package_list]):
self.remove(package_list)
else:
for package in package_list:
self.remove_package(package)
def operation_try_remove(self, package_list):
"""
Same relation as try_install has to install, except it removes
packages instead. Packages are removed one-by-one.
NOTE: package managers may reimplement this method
NOTE: no package-installation exceptions should be raised
"""
for package in package_list:
try:
self.remove_package(package)
except subprocess.CalledProcessError:
libcalamares.utils.warning("Could not remove package %s" % package)
### PACKAGE MANAGER IMPLEMENTATIONS
#
# Keep these alphabetical (presumably both by class name and backend name),
# even the Dummy implementation.
#
class PMPacman(PackageManager):
backend = "pacman"
def __init__(self):
import re
progress_match = re.compile("^\\((\\d+)/(\\d+)\\)")
def line_cb(line):
if line.startswith(":: "):
self.in_package_changes = "package" in line or "hooks" in line
else:
if self.in_package_changes and line.endswith("...\n"):
# Update the message, untranslated; do not change the
# progress percentage, since there may be more "installing..."
# lines in the output for the group, than packages listed
# explicitly. We don't know how to calculate proper progress.
global custom_status_message
custom_status_message = "pacman: " + line.strip()
libcalamares.job.setprogress(self.progress_fraction)
libcalamares.utils.debug(line)
self.in_package_changes = False
self.line_cb = line_cb
pacman = libcalamares.job.configuration.get("pacman", None)
if pacman is None:
pacman = dict()
if type(pacman) is not dict:
libcalamares.utils.warning("Job configuration *pacman* will be ignored.")
pacman = dict()
self.pacman_num_retries = pacman.get("num_retries", 0)
self.pacman_disable_timeout = pacman.get("disable_download_timeout", False)
self.pacman_needed_only = pacman.get("needed_only", False)
self.pacman_key = pacman.get("handle_keyrings", False)
self.pacman_pacconf = pacman.get("copy_pacconf", False)
self.pacman_requirements = pacman.get("requirements", [])
self.pacman_keyrings = pacman.get("keyrings", [])
def reset_progress(self):
self.in_package_changes = False
# These are globals
self.progress_fraction = (completed_packages * 1.0 / total_packages)
def run_pacman(self, command, callback=False):
"""
Call pacman in a loop until it is successful or the number of retries is exceeded
:param command: The pacman command to run
:param callback: An optional boolean that indicates if this pacman run should use the callback
:return:
"""
pacman_count = 0
while pacman_count <= self.pacman_num_retries:
pacman_count += 1
try:
if False: # callback:
host_env_process_output(command, self.line_cb)
else:
host_env_process_output(command)
return
except subprocess.CalledProcessError:
if pacman_count <= self.pacman_num_retries:
pass
else:
raise
def install(self, pkgs, from_local=False):
install_root = libcalamares.globalstorage.value("rootMountPoint")
cal_umask = os.umask(0)
for target in self.pacman_requirements:
dest = install_root + target["name"]
if not os.path.exists(dest):
mod = int(target["mode"],8)
os.mkdir(dest, mode=mod)
libcalamares.utils.debug("Mode: {!s}".format(oct(mod)))
libcalamares.utils.debug("Created: {!s}".format(dest))
path = join(install_root, "run")
os.chmod(path, 0o755)
os.umask(cal_umask)
f = "etc/resolv.conf"
if os.path.exists(join("/",f)):
shutil.copy2(join("/",f), join(install_root, f))
command = ["pacman"]
cachedir = join(install_root, "var/cache/pacman/pkg")
dbdir = join(install_root, "var/lib/pacman")
pacman_args = ["--root", install_root, "--dbpath", dbdir, "--cachedir", cachedir]
command.extend(pacman_args)
# Don't ask for user intervention, take the default action
command.append("--noconfirm")
# Don't report download progress for each file
command.append("--noprogressbar")
if self.pacman_needed_only is True:
command.append("--needed")
if self.pacman_disable_timeout is True:
command.append("--disable-download-timeout")
if from_local:
command.append("-U")
else:
command.append("-Sy")
command += pkgs
libcalamares.utils.debug("Command: {!s}".format(command))
self.reset_progress()
self.run_pacman(command, True)
if self.pacman_key:
self.init_keyring()
self.populate_keyring()
if self.pacman_pacconf:
f = "etc/pacman.conf"
if os.path.exists(join("/",f)):
shutil.copy2(join("/",f), join(install_root, f))
def remove(self, pkgs):
self.reset_progress()
self.run_pacman(["pacman", "-Rs", "--noconfirm"] + pkgs, True)
def init_keyring(self):
target_env_process_output(["pacman-key", "--init"])
def populate_keyring(self):
target_env_process_output(["pacman-key", "--populate"] + self.pacman_keyrings)
# Collect all the subclasses of PackageManager defined above,
# and index them based on the backend property of each class.
backend_managers = [
(c.backend, c)
for c in globals().values()
if type(c) is abc.ABCMeta and issubclass(c, PackageManager) and c.backend]
def subst_locale(plist):
"""
Returns a locale-aware list of packages, based on @p plist.
Package names that contain LOCALE are localized with the
BCP47 name of the chosen system locale; if the system
locale is 'en' (e.g. English, US) then these localized
packages are dropped from the list.
@param plist: list[str|dict]
Candidate packages to install.
@return: list[str|dict]
"""
locale = libcalamares.globalstorage.value("locale")
if not locale:
# It is possible to skip the locale-setting entirely.
# Then pretend it is "en", so that {LOCALE}-decorated
# package names are removed from the list.
locale = "en"
ret = []
for packagedata in plist:
if isinstance(packagedata, str):
packagename = packagedata
else:
packagename = packagedata["package"]
# Update packagename: substitute LOCALE, and drop packages
# if locale is en and LOCALE is in the package name.
if locale != "en":
packagename = Template(packagename).safe_substitute(LOCALE=locale)
elif 'LOCALE' in packagename:
packagename = None
if packagename is not None:
# Put it back in packagedata
if isinstance(packagedata, str):
packagedata = packagename
else:
packagedata["package"] = packagename
ret.append(packagedata)
return ret
def run_operations(pkgman, entry):
"""
Call package manager with suitable parameters for the given
package actions.
:param pkgman: PackageManager
This is the manager that does the actual work.
:param entry: dict
Keys are the actions -- e.g. "install" -- to take, and the values
are the (list of) packages to apply the action to. The actions are
not iterated in a specific order, so it is recommended to use only
one action per dictionary. The list of packages may be package
names (strings) or package information dictionaries with pre-
and post-scripts.
"""
global group_packages, completed_packages, mode_packages
for key in entry.keys():
package_list = subst_locale(entry[key])
group_packages = len(package_list)
if key == "install":
_change_mode(INSTALL)
pkgman.operation_install(package_list)
elif key == "try_install":
_change_mode(INSTALL)
pkgman.operation_try_install(package_list)
elif key == "remove":
_change_mode(REMOVE)
pkgman.operation_remove(package_list)
elif key == "try_remove":
_change_mode(REMOVE)
pkgman.operation_try_remove(package_list)
elif key == "localInstall":
_change_mode(INSTALL)
pkgman.operation_install(package_list, from_local=True)
elif key == "source":
libcalamares.utils.debug("Package-list from {!s}".format(entry[key]))
else:
libcalamares.utils.warning("Unknown package-operation key {!s}".format(key))
completed_packages += len(package_list)
libcalamares.job.setprogress(completed_packages * 1.0 / total_packages)
libcalamares.utils.debug("Pretty name: {!s}, setting progress..".format(pretty_name()))
group_packages = 0
_change_mode(None)
def run():
"""
Calls routine with detected package manager to install locale packages
or remove drivers not needed on the installed system.
:return:
"""
global mode_packages, total_packages, completed_packages, group_packages
backend = libcalamares.job.configuration.get("backend")
for identifier, impl in backend_managers:
if identifier == backend:
pkgman = impl()
break
else:
return "Bad backend", "backend=\"{}\"".format(backend)
if not libcalamares.globalstorage.value("hasInternet"):
libcalamares.utils.warning( "Package installation has been skipped: no internet" )
return None
operations = libcalamares.job.configuration.get("operations", [])
# if libcalamares.globalstorage.contains("packageOperations"):
# operations += libcalamares.globalstorage.value("packageOperations")
if libcalamares.job.configuration.get("base_init"):
base_init = libcalamares.job.configuration.get("base_init", None)
if libcalamares.globalstorage.contains("netinstallAdd"):
data = libcalamares.globalstorage.value("netinstallAdd")
init_provider = data[0]["name"]
libcalamares.utils.debug("Init provider: {!s}".format(init_provider))
if base_init is not None:
init_pkg = "-".join([base_init, init_provider])
libcalamares.utils.debug("Package added: {!s}".format(init_pkg))
operations[0]["install"].append(init_pkg)
if init_provider is not None:
libcalamares.globalstorage.insert("initProvider", init_provider)
libcalamares.globalstorage.insert("packageOperationsBasestrap", operations)
mode_packages = None
total_packages = 0
completed_packages = 0
for op in operations:
for packagelist in op.values():
total_packages += len(subst_locale(packagelist))
if not total_packages:
# Avoids potential divide-by-zero in progress reporting
return None
for entry in operations:
group_packages = 0
libcalamares.utils.debug(pretty_name())
try:
run_operations(pkgman, entry)
except subprocess.CalledProcessError as e:
libcalamares.utils.warning(str(e))
libcalamares.utils.debug("stdout:" + str(e.stdout))
libcalamares.utils.debug("stderr:" + str(e.stderr))
return (_("Package Manager error"),
_("The package manager could not make changes to the installed system. The command <pre>{!s}</pre> returned error code {!s}.")
.format(e.cmd, e.returncode))
mode_packages = None
libcalamares.job.setprogress(1.0)
return None

View File

@@ -1,7 +0,0 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
---
type: "job"
name: "basestrap"
interface: "python"
script: "main.py"

View File

@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
#
if( NOT Calamares_WITH_QML )
calamares_skip_module( "freebsddisk (QML is not supported in this build)" )
return()
endif()
calamares_add_plugin( freebsddisk
TYPE viewmodule
EXPORT_MACRO PLUGINDLLEXPORT_PRO
SOURCES
FreeBSDDiskViewStep.cpp
RESOURCES
freebsddisk.qrc
SHARED_LIB
)

View File

@@ -0,0 +1,42 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Calamares is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Calamares. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSES/GPL-3.0
*/
#include "FreeBSDDiskViewStep.h"
FreeBSDDiskViewStep::FreeBSDDiskViewStep( QObject* parent )
: Calamares::QmlViewStep( parent )
{
}
FreeBSDDiskViewStep::~FreeBSDDiskViewStep() {}
QString
FreeBSDDiskViewStep::prettyName() const
{
return tr( "Disk Setup" );
}
void
FreeBSDDiskViewStep::setConfigurationMap( const QVariantMap& configurationMap )
{
Calamares::QmlViewStep::setConfigurationMap( configurationMap ); // call parent implementation last
}
CALAMARES_PLUGIN_FACTORY_DEFINITION( FreeBSDDiskViewStepFactory, registerPlugin< FreeBSDDiskViewStep >(); )

View File

@@ -0,0 +1,44 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Calamares is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Calamares. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSES/GPL-3.0
*/
#ifndef FREEBSDDISKVIEWSTEP_H
#define FREEBSDDISKVIEWSTEP_H
#include "DllMacro.h"
#include "utils/PluginFactory.h"
#include "viewpages/QmlViewStep.h"
class PLUGINDLLEXPORT FreeBSDDiskViewStep : public Calamares::QmlViewStep
{
Q_OBJECT
public:
FreeBSDDiskViewStep( QObject* parent = nullptr );
virtual ~FreeBSDDiskViewStep() override;
QString prettyName() const override;
void setConfigurationMap( const QVariantMap& configurationMap ) override;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( FreeBSDDiskViewStepFactory )
#endif

View File

@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
#
# The *freebsddisk* module can be used to pick a disk
# as an installer step. This module supports ZFSroot
# on one whole disk, and UFSroot on one whole disk.
#
---
qmlSearch: both

View File

@@ -0,0 +1,35 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Calamares is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Calamares. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSES/GPL-3.0
*/
import io.calamares.ui 1.0
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Window 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.1
Item {
Text {
anchors.top: parent.top
anchors.topMargin: 10
text: "Select a disk on which to install FreeBSD."
}
}

View File

@@ -0,0 +1,5 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="freebsddisk.qml">freebsddisk.qml</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
# SPDX-License-Identifier: GPL-3.0-or-later
calamares_add_plugin( mobile
TYPE viewmodule
EXPORT_MACRO PLUGINDLLEXPORT_PRO
SOURCES
Config.cpp
MobileQmlViewStep.cpp
PartitionJob.cpp
UsersJob.cpp
RESOURCES
mobile.qrc
SHARED_LIB
)

221
modules/mobile/Config.cpp Normal file
View File

@@ -0,0 +1,221 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include "Config.h"
#include "PartitionJob.h"
#include "UsersJob.h"
#include "ViewManager.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include <QVariant>
Config::Config( QObject* parent )
: QObject( parent )
{
}
void
Config::setConfigurationMap( const QVariantMap& cfgMap )
{
using namespace CalamaresUtils;
if ( getBool( cfgMap, "bogus", false ) )
{
cWarning() << "Configuration key \"bogus\" is still set for *mobile*";
}
m_osName = getString( cfgMap, "osName", "(unknown)" );
m_arch = getString( cfgMap, "arch", "(unknown)" );
m_device = getString( cfgMap, "device", "(unknown)" );
m_userInterface = getString( cfgMap, "userInterface", "(unknown)" );
m_version = getString( cfgMap, "version", "(unknown)" );
m_reservedUsernames = getStringList( cfgMap, "reservedUsernames", QStringList { "adm", "at ", "bin", "colord",
"cron", "cyrus", "daemon", "ftp", "games", "geoclue", "guest", "halt", "lightdm", "lp", "mail", "man",
"messagebus", "news", "nobody", "ntp", "operator", "polkitd", "postmaster", "pulse", "root", "shutdown",
"smmsp", "squid", "sshd", "sync", "uucp", "vpopmail", "xfs" } );
// ensure m_cmdUsermod matches m_username
m_username = getString( cfgMap, "username", "user" );
m_userPasswordNumeric = getBool( cfgMap, "userPasswordNumeric", true );
m_builtinVirtualKeyboard = getBool( cfgMap, "builtinVirtualKeyboard", true );
m_featureSshd = getBool( cfgMap, "featureSshd", true );
m_featureFsType = getBool( cfgMap, "featureFsType", false );
m_cmdLuksFormat = getString( cfgMap, "cmdLuksFormat", "cryptsetup luksFormat --use-random" );
m_cmdLuksOpen = getString( cfgMap, "cmdLuksOpen", "cryptsetup luksOpen" );
m_cmdMount = getString( cfgMap, "cmdMount", "mount" );
m_targetDeviceRoot = getString( cfgMap, "targetDeviceRoot", "/dev/unknown" );
m_targetDeviceRootInternal = getString( cfgMap, "targetDeviceRootInternal", "" );
m_cmdMkfsRootBtrfs = getString( cfgMap, "cmdMkfsRootBtrfs", "mkfs.btrfs -L 'unknownOS_root'" );
m_cmdMkfsRootExt4 = getString( cfgMap, "cmdMkfsRootExt4", "mkfs.ext4 -L 'unknownOS_root'" );
m_cmdMkfsRootF2fs = getString( cfgMap, "cmdMkfsRootF2fs", "mkfs.f2fs -l 'unknownOS_root'" );
m_fsList = getStringList( cfgMap, "fsModel", QStringList { "ext4", "f2fs", "btrfs" } );
m_defaultFs = getString( cfgMap, "defaultFs", "ext4" );
m_fsIndex = m_fsList.indexOf( m_defaultFs );
m_fsType = m_defaultFs;
m_cmdInternalStoragePrepare = getString( cfgMap, "cmdInternalStoragePrepare", "ondev-internal-storage-prepare" );
m_cmdPasswd = getString( cfgMap, "cmdPasswd", "passwd" );
m_cmdUsermod = getString( cfgMap, "cmdUsermod", "xargs -I{} -n1 usermod -m -d /home/{} -l {} -c {} user");
m_cmdSshdEnable = getString( cfgMap, "cmdSshdEnable", "systemctl enable sshd.service" );
m_cmdSshdDisable = getString( cfgMap, "cmdSshdDisable", "systemctl disable sshd.service" );
m_cmdSshdUseradd = getString( cfgMap, "cmdSshdUseradd", "useradd -G wheel -m" );
}
Calamares::JobList
Config::createJobs()
{
QList< Calamares::job_ptr > list;
QString cmdSshd = m_isSshEnabled ? m_cmdSshdEnable : m_cmdSshdDisable;
/* Put users job in queue (should run after unpackfs) */
Calamares::Job* j = new UsersJob( m_featureSshd,
m_cmdPasswd,
m_cmdUsermod,
cmdSshd,
m_cmdSshdUseradd,
m_isSshEnabled,
m_username,
m_userPassword,
m_sshdUsername,
m_sshdPassword );
list.append( Calamares::job_ptr( j ) );
return list;
}
void
Config::runPartitionJobThenLeave( bool b )
{
Calamares::ViewManager* v = Calamares::ViewManager::instance();
QString cmdMkfsRoot;
if ( m_fsType == QStringLiteral( "btrfs" ) )
{
cmdMkfsRoot = m_cmdMkfsRootBtrfs;
}
else if ( m_fsType == QStringLiteral( "f2fs" ) )
{
cmdMkfsRoot = m_cmdMkfsRootF2fs;
}
else if ( m_fsType == QStringLiteral( "ext4" ) )
{
cmdMkfsRoot = m_cmdMkfsRootExt4;
}
else
{
v->onInstallationFailed( "Unknown filesystem: '" + m_fsType + "'", "" );
}
/* HACK: run partition job
* The "mobile" module has two jobs, the partition job and the users job.
* If we added both of them in Config::createJobs(), Calamares would run
* them right after each other. But we need the "unpackfs" module to run
* inbetween, that's why as workaround, the partition job is started here.
* To solve this properly, we would need to place the partition job in an
* own module and pass everything via globalstorage. But then we might as
* well refactor everything so we can unify the mobile's partition job with
* the proper partition job from Calamares. */
Calamares::Job* j = new PartitionJob( m_cmdInternalStoragePrepare,
m_cmdLuksFormat,
m_cmdLuksOpen,
cmdMkfsRoot,
m_cmdMount,
m_targetDeviceRoot,
m_targetDeviceRootInternal,
m_installFromExternalToInternal,
m_isFdeEnabled,
m_fdePassword );
Calamares::JobResult res = j->exec();
if ( res )
{
v->next();
}
else
{
v->onInstallationFailed( res.message(), res.details() );
}
}
void
Config::setUsername( const QString& username )
{
m_username = username;
emit usernameChanged( m_username );
}
void
Config::setUserPassword( const QString& userPassword )
{
m_userPassword = userPassword;
emit userPasswordChanged( m_userPassword );
}
void
Config::setSshdUsername( const QString& sshdUsername )
{
m_sshdUsername = sshdUsername;
emit sshdUsernameChanged( m_sshdUsername );
}
void
Config::setSshdPassword( const QString& sshdPassword )
{
m_sshdPassword = sshdPassword;
emit sshdPasswordChanged( m_sshdPassword );
}
void
Config::setIsSshEnabled( const bool isSshEnabled )
{
m_isSshEnabled = isSshEnabled;
}
void
Config::setFdePassword( const QString& fdePassword )
{
m_fdePassword = fdePassword;
emit fdePasswordChanged( m_fdePassword );
}
void
Config::setIsFdeEnabled( const bool isFdeEnabled )
{
m_isFdeEnabled = isFdeEnabled;
}
void
Config::setInstallFromExternalToInternal( const bool val )
{
m_installFromExternalToInternal = val;
}
void
Config::setFsType( int idx )
{
if ( idx >= 0 && idx < m_fsList.length() )
{
setFsType( m_fsList[ idx ] );
}
}
void
Config::setFsType( const QString& fsType )
{
if ( fsType != m_fsType )
{
m_fsType = fsType;
emit fsTypeChanged( m_fsType );
}
}
void
Config::setFsIndex( const int fsIndex )
{
m_fsIndex = fsIndex;
emit fsIndexChanged( m_fsIndex );
}

207
modules/mobile/Config.h Normal file
View File

@@ -0,0 +1,207 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
#pragma once
#include "Job.h"
#include <QObject>
#include <memory>
class Config : public QObject
{
Q_OBJECT
/* installer UI */
Q_PROPERTY( bool builtinVirtualKeyboard READ builtinVirtualKeyboard CONSTANT FINAL )
/* welcome */
Q_PROPERTY( QString osName READ osName CONSTANT FINAL )
Q_PROPERTY( QString arch READ arch CONSTANT FINAL )
Q_PROPERTY( QString device READ device CONSTANT FINAL )
Q_PROPERTY( QString userInterface READ userInterface CONSTANT FINAL )
Q_PROPERTY( QString version READ version CONSTANT FINAL )
/* reserved usernames (user_pass, ssh_credentials )*/
Q_PROPERTY( QStringList reservedUsernames READ reservedUsernames CONSTANT FINAL )
/* default user */
Q_PROPERTY( QString username READ username WRITE setUsername NOTIFY usernameChanged )
Q_PROPERTY( QString userPassword READ userPassword WRITE setUserPassword NOTIFY userPasswordChanged )
Q_PROPERTY( bool userPasswordNumeric READ userPasswordNumeric CONSTANT FINAL )
/* ssh server + credentials */
Q_PROPERTY( bool featureSshd READ featureSshd CONSTANT FINAL )
Q_PROPERTY( QString sshdUsername READ sshdUsername WRITE setSshdUsername NOTIFY sshdUsernameChanged )
Q_PROPERTY( QString sshdPassword READ sshdPassword WRITE setSshdPassword NOTIFY sshdPasswordChanged )
Q_PROPERTY( bool isSshEnabled READ isSshEnabled WRITE setIsSshEnabled )
/* full disk encryption */
Q_PROPERTY( QString fdePassword READ fdePassword WRITE setFdePassword NOTIFY fdePasswordChanged )
Q_PROPERTY( bool isFdeEnabled READ isFdeEnabled WRITE setIsFdeEnabled )
/* filesystem selection */
Q_PROPERTY( QString fsType READ fsType WRITE setFsType NOTIFY fsTypeChanged )
Q_PROPERTY( bool featureFsType READ featureFsType CONSTANT FINAL )
Q_PROPERTY( QStringList fsList READ fsList CONSTANT FINAL )
Q_PROPERTY( QString defaultFs READ defaultFs CONSTANT FINAL )
Q_PROPERTY( int fsIndex READ fsIndex WRITE setFsIndex NOTIFY fsIndexChanged )
/* partition job */
Q_PROPERTY( bool runPartitionJobThenLeave READ runPartitionJobThenLeaveDummy WRITE runPartitionJobThenLeave )
Q_PROPERTY( QString cmdInternalStoragePrepare READ cmdInternalStoragePrepare CONSTANT FINAL )
Q_PROPERTY( QString cmdLuksFormat READ cmdLuksFormat CONSTANT FINAL )
Q_PROPERTY( QString cmdLuksOpen READ cmdLuksOpen CONSTANT FINAL )
Q_PROPERTY( QString cmdMount READ cmdMount CONSTANT FINAL )
Q_PROPERTY( QString targetDeviceRoot READ targetDeviceRoot CONSTANT FINAL )
Q_PROPERTY( QString targetDeviceRootInternal READ targetDeviceRootInternal CONSTANT FINAL )
Q_PROPERTY(
bool installFromExternalToInternal READ installFromExternalToInternal WRITE setInstallFromExternalToInternal )
/* users job */
Q_PROPERTY( QString cmdSshdEnable READ cmdSshdEnable CONSTANT FINAL )
Q_PROPERTY( QString cmdSshdDisable READ cmdSshdDisable CONSTANT FINAL )
public:
Config( QObject* parent = nullptr );
void setConfigurationMap( const QVariantMap& );
Calamares::JobList createJobs();
/* installer UI */
bool builtinVirtualKeyboard() { return m_builtinVirtualKeyboard; }
/* welcome */
QString osName() const { return m_osName; }
QString arch() const { return m_arch; }
QString device() const { return m_device; }
QString userInterface() const { return m_userInterface; }
QString version() const { return m_version; }
/* reserved usernames (user_pass, ssh_credentials) */
QStringList reservedUsernames() const { return m_reservedUsernames; };
/* user */
QString username() const { return m_username; }
QString userPassword() const { return m_userPassword; }
void setUsername( const QString& username );
void setUserPassword( const QString& userPassword );
bool userPasswordNumeric() const { return m_userPasswordNumeric; }
/* ssh server + credentials */
bool featureSshd() { return m_featureSshd; }
QString sshdUsername() const { return m_sshdUsername; }
QString sshdPassword() const { return m_sshdPassword; }
bool isSshEnabled() { return m_isSshEnabled; }
void setSshdUsername( const QString& sshdUsername );
void setSshdPassword( const QString& sshdPassword );
void setIsSshEnabled( bool isSshEnabled );
/* full disk encryption */
QString fdePassword() const { return m_fdePassword; }
bool isFdeEnabled() { return m_isFdeEnabled; }
void setFdePassword( const QString& fdePassword );
void setIsFdeEnabled( bool isFdeEnabled );
/* filesystem selection */
bool featureFsType() { return m_featureFsType; };
QString fsType() const { return m_fsType; };
void setFsType( int idx );
void setFsType( const QString& fsType );
QStringList fsList() const { return m_fsList; };
int fsIndex() const { return m_fsIndex; };
void setFsIndex( const int fsIndex );
QString defaultFs() const { return m_defaultFs; };
/* partition job */
bool runPartitionJobThenLeaveDummy() { return 0; }
void runPartitionJobThenLeave( bool b );
QString cmdInternalStoragePrepare() const { return m_cmdInternalStoragePrepare; }
QString cmdLuksFormat() const { return m_cmdLuksFormat; }
QString cmdLuksOpen() const { return m_cmdLuksOpen; }
QString cmdMkfsRootBtrfs() const { return m_cmdMkfsRootBtrfs; }
QString cmdMkfsRootExt4() const { return m_cmdMkfsRootExt4; }
QString cmdMkfsRootF2fs() const { return m_cmdMkfsRootF2fs; }
QString cmdMount() const { return m_cmdMount; }
QString targetDeviceRoot() const { return m_targetDeviceRoot; }
QString targetDeviceRootInternal() const { return m_targetDeviceRootInternal; }
bool installFromExternalToInternal() { return m_installFromExternalToInternal; }
void setInstallFromExternalToInternal( const bool val );
/* users job */
QString cmdPasswd() const { return m_cmdPasswd; }
QString cmdUsermod() const { return m_cmdUsermod; }
QString cmdSshdEnable() const { return m_cmdSshdEnable; }
QString cmdSshdDisable() const { return m_cmdSshdDisable; }
QString cmdSshdUseradd() const { return m_cmdSshdUseradd; }
private:
/* installer UI */
bool m_builtinVirtualKeyboard;
/* welcome */
QString m_osName;
QString m_arch;
QString m_device;
QString m_userInterface;
QString m_version;
/* reserved usernames (user_pass, ssh_credentials) */
QStringList m_reservedUsernames;
/* default user */
QString m_username;
QString m_userPassword;
bool m_userPasswordNumeric;
/* ssh server + credentials */
bool m_featureSshd = false;
QString m_sshdUsername;
QString m_sshdPassword;
bool m_isSshEnabled = false;
/* full disk encryption */
QString m_fdePassword;
bool m_isFdeEnabled = false;
/* filesystem selection */
bool m_featureFsType = false;
QString m_defaultFs;
QString m_fsType;
// Index of the currently selected filesystem in UI.
int m_fsIndex = -1;
QStringList m_fsList;
/* partition job */
QString m_cmdInternalStoragePrepare;
QString m_cmdLuksFormat;
QString m_cmdLuksOpen;
QString m_cmdMkfsRootBtrfs;
QString m_cmdMkfsRootExt4;
QString m_cmdMkfsRootF2fs;
QString m_cmdMount;
QString m_targetDeviceRoot;
QString m_targetDeviceRootInternal;
bool m_installFromExternalToInternal = false;
/* users job */
QString m_cmdPasswd;
QString m_cmdUsermod;
QString m_cmdSshdEnable;
QString m_cmdSshdDisable;
QString m_cmdSshdUseradd;
signals:
/* booleans we don't read from QML (like isSshEnabled) don't need a signal */
/* default user */
void userPasswordChanged( QString userPassword );
void usernameChanged( QString username );
/* ssh server + credentials */
void sshdUsernameChanged( QString sshdUsername );
void sshdPasswordChanged( QString sshdPassword );
/* full disk encryption */
void fdePasswordChanged( QString fdePassword );
void fsTypeChanged( QString fsType );
void fsIndexChanged( int fsIndex );
};

View File

@@ -0,0 +1,73 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include "MobileQmlViewStep.h"
#include "Branding.h"
#include "GlobalStorage.h"
#include "locale/TranslationsModel.h"
#include "modulesystem/ModuleManager.h"
#include "utils/Dirs.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include <QProcess>
CALAMARES_PLUGIN_FACTORY_DEFINITION( MobileQmlViewStepFactory, registerPlugin< MobileQmlViewStep >(); )
void
MobileQmlViewStep::setConfigurationMap( const QVariantMap& configurationMap )
{
m_config->setConfigurationMap( configurationMap );
Calamares::QmlViewStep::setConfigurationMap( configurationMap );
}
MobileQmlViewStep::MobileQmlViewStep( QObject* parent )
: Calamares::QmlViewStep( parent )
, m_config( new Config( this ) )
{
}
void
MobileQmlViewStep::onLeave()
{
return;
}
bool
MobileQmlViewStep::isNextEnabled() const
{
return false;
}
bool
MobileQmlViewStep::isBackEnabled() const
{
return false;
}
bool
MobileQmlViewStep::isAtBeginning() const
{
return true;
}
bool
MobileQmlViewStep::isAtEnd() const
{
return true;
}
Calamares::JobList
MobileQmlViewStep::jobs() const
{
return m_config->createJobs();
}
QObject*
MobileQmlViewStep::getConfig()
{
return m_config;
}

View File

@@ -0,0 +1,39 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef PARTITION_QMLVIEWSTEP_H
#define PARTITION_QMLVIEWSTEP_H
#include "Config.h"
#include "utils/PluginFactory.h"
#include "viewpages/QmlViewStep.h"
#include <DllMacro.h>
#include <QObject>
#include <QVariantMap>
class PLUGINDLLEXPORT MobileQmlViewStep : public Calamares::QmlViewStep
{
Q_OBJECT
public:
explicit MobileQmlViewStep( QObject* parent = nullptr );
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 onLeave() override;
QObject* getConfig() override;
private:
Config* m_config;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( MobileQmlViewStepFactory )
#endif // PARTITION_QMLVIEWSTEP_H

View File

@@ -0,0 +1,136 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include "PartitionJob.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "Settings.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include <QDir>
#include <QFileInfo>
PartitionJob::PartitionJob( const QString& cmdInternalStoragePrepare,
const QString& cmdLuksFormat,
const QString& cmdLuksOpen,
const QString& cmdMkfsRoot,
const QString& cmdMount,
const QString& targetDeviceRoot,
const QString& targetDeviceRootInternal,
bool installFromExternalToInternal,
bool isFdeEnabled,
const QString& password )
: Calamares::Job()
, m_cmdInternalStoragePrepare( cmdInternalStoragePrepare )
, m_cmdLuksFormat( cmdLuksFormat )
, m_cmdLuksOpen( cmdLuksOpen )
, m_cmdMkfsRoot( cmdMkfsRoot )
, m_cmdMount( cmdMount )
, m_targetDeviceRoot( targetDeviceRoot )
, m_targetDeviceRootInternal( targetDeviceRootInternal )
, m_installFromExternalToInternal( installFromExternalToInternal )
, m_isFdeEnabled( isFdeEnabled )
, m_password( password )
{
}
QString
PartitionJob::prettyName() const
{
return "Creating and formatting installation partition";
}
/* Fill the "global storage", so the following jobs (like unsquashfs) work.
The code is similar to modules/partition/jobs/FillGlobalStorageJob.cpp in
Calamares. */
void
FillGlobalStorage( const QString device, const QString pathMount )
{
using namespace Calamares;
GlobalStorage* gs = JobQueue::instance()->globalStorage();
QVariantList partitions;
QVariantMap partition;
/* See mapForPartition() in FillGlobalStorageJob.cpp */
partition[ "device" ] = device;
partition[ "mountPoint" ] = "/";
partition[ "claimed" ] = true;
/* Ignored by calamares modules used in combination with the "mobile"
* module, so we can get away with leaving them empty for now. */
partition[ "uuid" ] = "";
partition[ "fsName" ] = "";
partition[ "fs" ] = "";
partitions << partition;
gs->insert( "partitions", partitions );
gs->insert( "rootMountPoint", pathMount );
}
Calamares::JobResult
PartitionJob::exec()
{
using namespace Calamares;
using namespace CalamaresUtils;
using namespace std;
const QString pathMount = "/mnt/install";
const QString cryptName = "calamares_crypt";
QString cryptDev = "/dev/mapper/" + cryptName;
QString passwordStdin = m_password + "\n";
QString dev = m_targetDeviceRoot;
QList< QPair< QStringList, QString > > commands = {};
if ( m_installFromExternalToInternal )
{
dev = m_targetDeviceRootInternal;
commands.append( {
{ { "sh", "-c", m_cmdInternalStoragePrepare }, QString() },
} );
}
commands.append( { { { "mkdir", "-p", pathMount }, QString() } } );
if ( m_isFdeEnabled )
{
commands.append( {
{ { "sh", "-c", m_cmdLuksFormat + " " + dev }, passwordStdin },
{ { "sh", "-c", m_cmdLuksOpen + " " + dev + " " + cryptName }, passwordStdin },
{ { "sh", "-c", m_cmdMkfsRoot + " " + cryptDev }, QString() },
{ { "sh", "-c", m_cmdMount + " " + cryptDev + " " + pathMount }, QString() },
} );
}
else
{
commands.append( { { { "sh", "-c", m_cmdMkfsRoot + " " + dev }, QString() },
{ { "sh", "-c", m_cmdMount + " " + dev + " " + pathMount }, QString() } } );
}
foreach ( auto command, commands )
{
const QStringList args = command.first;
const QString stdInput = command.second;
const QString pathRoot = "/";
ProcessResult res
= System::runCommand( System::RunLocation::RunInHost, args, pathRoot, stdInput, chrono::seconds( 600 ) );
if ( res.getExitCode() )
{
return JobResult::error( "Command failed:<br><br>"
"'"
+ args.join( " " )
+ "'<br><br>"
" with output:<br><br>"
"'"
+ res.getOutput() + "'" );
}
}
FillGlobalStorage( m_isFdeEnabled ? cryptDev : dev, pathMount );
return JobResult::ok();
}

View File

@@ -0,0 +1,38 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
#pragma once
#include "Job.h"
class PartitionJob : public Calamares::Job
{
Q_OBJECT
public:
PartitionJob( const QString& cmdInternalStoragePrepare,
const QString& cmdLuksFormat,
const QString& cmdLuksOpen,
const QString& cmdMkfsRoot,
const QString& cmdMount,
const QString& targetDeviceRoot,
const QString& targetDeviceRootInternal,
bool installFromExternalToInternal,
bool isFdeEnabled,
const QString& password );
QString prettyName() const override;
Calamares::JobResult exec() override;
Calamares::JobList createJobs();
private:
QString m_cmdInternalStoragePrepare;
QString m_cmdLuksFormat;
QString m_cmdLuksOpen;
QString m_cmdMkfsRoot;
QString m_cmdMount;
QString m_targetDeviceRoot;
QString m_targetDeviceRootInternal;
bool m_installFromExternalToInternal;
bool m_isFdeEnabled;
QString m_password;
};

View File

@@ -0,0 +1,92 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include "UsersJob.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "Settings.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include <QDir>
#include <QFileInfo>
UsersJob::UsersJob( bool featureSshd,
const QString& cmdPasswd,
const QString& cmdUsermod,
const QString& cmdSshd,
const QString& cmdSshdUseradd,
bool isSshEnabled,
const QString& username,
const QString& password,
const QString& sshdUsername,
const QString& sshdPassword )
: Calamares::Job()
, m_featureSshd( featureSshd )
, m_cmdPasswd( cmdPasswd )
, m_cmdUsermod( cmdUsermod )
, m_cmdSshd( cmdSshd )
, m_cmdSshdUseradd( cmdSshdUseradd )
, m_isSshEnabled( isSshEnabled )
, m_username( username )
, m_password( password )
, m_sshdUsername( sshdUsername )
, m_sshdPassword( sshdPassword )
{
}
QString
UsersJob::prettyName() const
{
return "Configuring users";
}
Calamares::JobResult
UsersJob::exec()
{
using namespace Calamares;
using namespace CalamaresUtils;
using namespace std;
QList< QPair< QStringList, QString > > commands = {
{ { "sh", "-c", m_cmdUsermod }, m_username + "\n" }
};
commands.append( { { "sh", "-c", m_cmdPasswd + " " + m_username }, m_password + "\n" + m_password + "\n" } );
if ( m_featureSshd )
{
commands.append( { { "sh", "-c", m_cmdSshd }, QString() } );
if ( m_isSshEnabled )
{
commands.append( { { "sh", "-c", m_cmdSshdUseradd + " " + m_sshdUsername }, QString() } );
commands.append(
{ { "sh", "-c", m_cmdPasswd + " " + m_sshdUsername }, m_sshdPassword + "\n" + m_sshdPassword + "\n" } );
}
}
foreach ( auto command, commands )
{
auto location = System::RunLocation::RunInTarget;
const QString pathRoot = "/";
const QStringList args = command.first;
const QString stdInput = command.second;
ProcessResult res = System::runCommand( location, args, pathRoot, stdInput, chrono::seconds( 30 ) );
if ( res.getExitCode() )
{
return JobResult::error( "Command failed:<br><br>"
"'"
+ args.join( " " )
+ "'<br><br>"
" with output:<br><br>"
"'"
+ res.getOutput() + "'" );
}
}
return JobResult::ok();
}

38
modules/mobile/UsersJob.h Normal file
View File

@@ -0,0 +1,38 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
#pragma once
#include "Job.h"
class UsersJob : public Calamares::Job
{
Q_OBJECT
public:
UsersJob( bool featureSshd,
const QString& cmdPasswd,
const QString& cmdUsermod,
const QString& cmdSshd,
const QString& cmdSshdUseradd,
bool isSshEnabled,
const QString& username,
const QString& password,
const QString& sshdUsername,
const QString& sshdPassword );
QString prettyName() const override;
Calamares::JobResult exec() override;
Calamares::JobList createJobs();
private:
bool m_featureSshd;
QString m_cmdPasswd;
QString m_cmdUsermod;
QString m_cmdSshd;
QString m_cmdSshdUseradd;
bool m_isSshEnabled;
QString m_username;
QString m_password;
QString m_sshdUsername;
QString m_sshdPassword;
};

View File

@@ -0,0 +1,65 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
Text {
id: mainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
wrapMode: Text.WordWrap
text: "To protect your data in case your device gets stolen," +
" it is recommended to enable full disk encryption.<br>" +
"<br>" +
"If you enable full disk encryption, you will be asked for" +
" a password. Without this password, it is not possible to" +
" boot your device or access any data on it. Make sure that" +
" you don't lose this password!"
width: 200
}
Button {
id: firstButton
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: mainText.bottom
anchors.topMargin: 10
width: 200
text: qsTr("Enable")
onClicked: {
config.isFdeEnabled = true;
navNext();
}
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: firstButton.bottom
anchors.topMargin: 10
width: 200
text: qsTr("Disable")
onClicked: {
config.isFdeEnabled = false;
navNextFeature();
}
}
}

View File

@@ -0,0 +1,80 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
TextField {
id: password
anchors.top: parent.top
placeholderText: qsTr("Password")
inputMethodHints: Qt.ImhPreferLowercase
echoMode: TextInput.Password
onTextChanged: validatePassword(password, passwordRepeat,
errorText)
text: config.fdePassword
onActiveFocusChanged: {
if(activeFocus) {
Qt.inputMethod.update(Qt.ImQueryInput);
}
}
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 50
width: 200
}
TextField {
id: passwordRepeat
anchors.top: password.bottom
placeholderText: qsTr("Password (repeat)")
echoMode: TextInput.Password
onTextChanged: validatePassword(password, passwordRepeat,
errorText)
text: config.fdePassword
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 10
width: 200
}
Text {
id: errorText
anchors.top: passwordRepeat.bottom
visible: false
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 10
width: 200
wrapMode: Text.WordWrap
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: errorText.bottom
anchors.topMargin: 10
width: 200
text: qsTr("Continue")
onClicked: {
if (validatePassword(password, passwordRepeat, errorText)) {
config.fdePassword = password.text;
navNext();
}
}
}
}

View File

@@ -0,0 +1,59 @@
/* SPDX-FileCopyrightText: 2020 Undef <calamares@undef.tools>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
Text {
id: mainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
wrapMode: Text.WordWrap
text: "Select the filesystem for root partition. If unsure, leave the default."
width: 200
}
ComboBox {
id: fsTypeCB
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: mainText.bottom
anchors.topMargin: 10
width: 150
height: 30
editable: false
model: config.fsList
/* Save the current state on selection so it is there when the back button is pressed */
onActivated: config.fsType = fsTypeCB.currentText;
Component.onCompleted: fsTypeCB.currentIndex = find( config.fsType, Qt.MatchContains );
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: fsTypeCB.bottom
anchors.topMargin: 10
width: 200
text: qsTr("Continue")
onClicked: {
config.fsType = fsTypeCB.currentText;
navNextFeature();
}
}
}

View File

@@ -0,0 +1,60 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
Text {
id: mainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
wrapMode: Text.WordWrap
text: (function() {
var ret = "Once you hit 'install', the installation will begin." +
" It will typically take a few minutes. Do not power off the" +
" device until it is done.<br>";
if (config.installFromExternalToInternal) {
ret += "<b>After the installation, your device will shutdown" +
" automatically. You must remove the external storage" +
" (SD card) before booting again.</b>" +
"<br><br>" +
"Otherwise, your device will boot into the installer" +
" again, and not into the installed system."
} else {
ret += "Afterwards, it will reboot into the installed system.";
}
return ret;
}())
width: 200
}
Button {
id: firstButton
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: mainText.bottom
anchors.topMargin: 10
width: 200
text: qsTr("Install")
onClicked: navFinish()
}
}

View File

@@ -0,0 +1,64 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
Text {
id: mainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
wrapMode: Text.WordWrap
text: "The installation was started from an external storage medium." +
"<br>" +
"You can either install to the same medium and overwrite the" +
" installer, or install to the internal storage.<br>" +
"<br>" +
"Where would you like to install " + config.osName + "?"
width: 200
}
Button {
id: firstButton
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: mainText.bottom
anchors.topMargin: 10
width: 200
text: qsTr("Internal (eMMC)")
onClicked: {
config.installFromExternalToInternal = true;
navNext();
}
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: firstButton.bottom
anchors.topMargin: 10
width: 200
text: qsTr("External (SD card)")
onClicked: {
config.installFromExternalToInternal = false;
navNextFeature();
}
}
}

View File

@@ -0,0 +1,58 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
Text {
id: mainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
wrapMode: Text.WordWrap
text: "Are you sure that you want to overwrite the internal storage?" +
"<br><br>" +
"<b>All existing data on the device will be lost!</b>"
width: 200
}
Button {
id: firstButton
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: mainText.bottom
anchors.topMargin: 10
width: 200
text: qsTr("Yes")
onClicked: {
navNext();
}
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: firstButton.bottom
anchors.topMargin: 10
width: 200
text: qsTr("No")
onClicked: {
navBack();
}
}
}

173
modules/mobile/mobile.conf Normal file
View File

@@ -0,0 +1,173 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
#
# Commented out values are defaults.
# All commands are running with 'sh -c'.
---
# This entry exists only to keep the tests happy, remove it in
# any production configuration.
bogus: true
#######
### Target OS information
#######
## Operating System Name
# osName: "(unknown)"
## User Interface name (e.g. Plasma Mobile)
# userInterface: "(unknown)"
## User Interface assumes that the password is numeric (as of writing, this is
## the case with Plasma Mobile and Phosh)
# userPasswordNumeric: true
## OS version
# version: "(unknown)"
## Default username (for which the password will be set)
## Ensure also cmdUsermod command matches the default user, so it can be changed if desired.
# username: "user"
## reserved usernames (for user_pass username prompt and ssh_credentials)
# reservedUsernames:
# - adm
# - at
# - bin
# - colord
# - cron
# - cyrus
# - daemon
# - ftp
# - games
# - geoclue
# - guest
# - halt
# - lightdm
# - lp
# - mail
# - man
# - messagebus
# - news
# - nobody
# - ntp
# - operator
# - polkitd
# - postmaster
# - pulse
# - root
# - shutdown
# - smmsp
# - squid
# - sshd
# - sync
# - uucp
# - vpopmail
# - xfs
#######
### Target device information
#######
## Architecture (e.g. aarch64)
# arch: "(unknown)"
## Name of the device (e.g. PinePhone)
# device: "(unknown)"
## Partition that will be formatted and mounted (optionally with FDE) for the
## rootfs
# targetDeviceRoot: "/dev/unknown"
## Partition that will be formatted and mounted (optionally with FDE) for the
## rootfs, on internal storage. The installer OS must not set this, if it was
## booted from the internal storage (this is not checked in the mobile
## module!).
## If this is set, the user gets asked whether they want to install on internal
## or external storage. If the user chose internal storage,
## cmdInternalStoragePrepare (see below) runs before this partition gets
## formatted (see below). A note is displayed, that the device is powered off
## after installation and that the user should remove the external storage
## medium. So you need to adjust the installer OS to poweroff in that case, and
## not reboot. See postmarketos-ondev.git for reference.
# targetDeviceRootInternal: ""
######
### Installer Features
######
## Ask whether sshd should be enabled or not. If enabled, add a dedicated ssh
## user with proper username and password and suggest to change to key-based
## authentication after installation.
# featureSshd: true
## Ask the user, which filesystem to use.
# featureFsType: false
## Filesystems that the user can choose from.
#fsModel:
# - ext4
# - f2fs
# - btrfs
## Default filesystem to display in the dialog. If featureFsType is disabled,
## this gets used without asking the user.
# defaultFs: ext4
## Start Qt's virtual keyboard within the mobile module. Disable if you bring
## your own virtual keyboard (e.g. svkbd).
# builtinVirtualKeyboard: true
#######
### Commands running in the installer OS
#######
## Format the target partition with LUKS
## Arguments: <device>
## Stdin: password with \n
# cmdLuksFormat: "cryptsetup luksFormat --use-random"
## Open the formatted partition
## Arguments: <device> <mapping name>
## Stdin: password with \n
# cmdLuksOpen: "cryptsetup luksOpen"
## Format the rootfs with a file system
## Arguments: <device>
## Btrfs: to allow snapshots to work on the root subvolume, it is recommended that this
## command be a script which will create a subvolume and make it default
## An example can be found at:
## https://gitlab.com/mobian1/calamares-settings-mobian/-/merge_requests/2/diffs#diff-content-dde34f5f1c89e3dea63608c553bbc452dedf428f
# cmdMkfsRootBtrfs: "mkfs.btrfs -L 'unknownOS_root'"
# cmdMkfsRootExt4: "mkfs.ext4 -L 'unknownOS_root'"
# cmdMkfsRootF2fs: "mkfs.f2fs -l 'unknownOS_root'"
## Mount the partition after formatting with file system
## Arguments: <device> <mountpoint>
# cmdMount: "mount"
## When user selects installation from external storage to internal storage
## (see targetDeviceRootInternal above), use this command to prepare the
## internal storage medium. The command must create a partition table with
## two partitions (boot, root) and fill the boot partition. See the
## ondev-internal-storage-prepare.sh in postmarketos-ondev as example.
# cmdInternalStoragePrepare: "ondev-internal-storage-prepare"
#######
### Commands running in the target OS (chroot)
#######
## Change the username for the default user
## Stdin: username with \n
# cmdUsermod: "xargs -I{} -n1 usermod -m -d /home/{} -l {} -c {} user"
## Set the password for default user and sshd user
## Arguments: <username>
## Stdin: password twice, each time with \n
# cmdPasswd: "passwd"
## Enable or disable sshd
# cmdSshdEnable: "systemctl enable sshd.service"
# cmdSshdDisable: "systemctl disable sshd.service"
## Create the user for sshd
## Arguments: <username>
# cmdSshdUseradd: "useradd -G wheel -m"

390
modules/mobile/mobile.qml Normal file
View File

@@ -0,0 +1,390 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Page
{
property var screen: "welcome"
property var screenPrevious: []
property var titles: {
"welcome": null, /* titlebar disabled */
"install_target": "Installation target",
"install_target_confirm": "Warning",
"user_pass": "User password",
"ssh_confirm": "SSH server",
"ssh_credentials": "SSH credentials",
"fs_selection": "Root filesystem",
"fde_confirm": "Full disk encryption",
"fde_pass": "Full disk encryption",
"install_confirm": "Ready to install",
"wait": null
}
property var features: [
{"name": "welcome",
"screens": ["welcome"]},
{"name": "installTarget",
"screens": ["install_target", "install_target_confirm"]},
{"name": "userPassword",
"screens": ["user_pass"]},
{"name": "sshd",
"screens": ["ssh_confirm", "ssh_credentials"]},
{"name": "fsType",
"screens": ["fs_selection"]},
{"name": "fde",
"screens": ["fde_confirm", "fde_pass"]},
{"name": "installConfirm",
"screens": ["install_confirm", "wait"]}
]
property var featureIdByScreen: (function() {
/* Put "features" above into an index of screen name -> feature id:
* featureIdByScreen = {"welcome": 0, "user_pass": 1, ...} */
var ret = {};
for (var i=0; i<features.length; i++) {
for (var j=0; j<features[i]["screens"].length; j++) {
ret[ features[i]["screens"][j] ] = i;
}
}
return ret;
}())
/* Only allow characters, that can be typed in with osk-sdl
* (src/keyboard.cpp). Details in big comment in validatePassword(). */
property var allowed_chars:
/* layer 0 */ "abcdefghijklmnopqrstuvwxyz" +
/* layer 1 */ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
/* layer 2 */ "1234567890" + "@#$%&-_+()" + ",\"':;!?" +
/* layer 3 */ "~`|·√πτ÷×¶" + "©®£€¥^°*{}" + "\\/<>=[]" +
/* bottom row */ " ."
Item {
id: appContainer
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
anchors.bottom: inputPanel.top
Item {
width: parent.width
height: parent.height
Rectangle {
id: mobileNavigation
width: parent.width
height: 30
color: "#e6e4e1"
Layout.fillWidth: true
border.width: 1
border.color: "#a7a7a7"
anchors.left: parent.left
anchors.right: parent.right
RowLayout {
width: parent.width
height: parent.height
spacing: 6
Button {
Layout.leftMargin: 6
id: mobileBack
text: "<"
background: Rectangle {
implicitWidth: 10
implicitHeight: 7
border.color: "#c1bab5"
border.width: 1
radius: 4
color: mobileBack.down ? "#dbdbdb" : "#f2f2f2"
}
onClicked: navBack()
}
Rectangle {
implicitHeight: 10
Layout.fillWidth: true
color: "#e6e4e1"
Text {
id: mobileTitle
text: ""
color: "#303638"
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
}
}
Rectangle {
color: "#e6e4e1"
Layout.rightMargin: 6
implicitWidth: 32
implicitHeight: 30
id: filler
}
}
}
Loader {
id: load
anchors.left: parent.left
anchors.top: mobileNavigation.bottom
anchors.right: parent.right
}
}
}
InputPanel {
id: inputPanel
y: Qt.inputMethod.visible ? parent.height - inputPanel.height : parent.height
visible: config.builtinVirtualKeyboard
anchors.left: parent.left
anchors.right: parent.right
}
Timer {
id: timer
}
function skipFeatureInstallTarget() {
return config.targetDeviceRootInternal == "";
}
/* Navigation related */
function navTo(name, historyPush=true) {
console.log("Navigating to screen: " + name);
if (historyPush)
screenPrevious.push(screen);
screen = name;
load.source = name + ".qml";
mobileNavigation.visible = (titles[name] !== null);
mobileTitle.text = "<b>" + titles[name] + "</b>";
Qt.inputMethod.hide();
}
function navFinish() {
/* Show a waiting screen and wait a second (so it can render). The big
* comment in Config.cpp::runPartitionJobThenLeave() explains why this
* is necessary. */
navTo("wait");
timer.interval = 1000;
timer.repeat = false;
timer.triggered.connect(function() {
/* Trigger Config.cpp::runPartitionJobThenLeave(). (We could expose
* the function directly with qmlRegisterSingletonType somehow, but
* I haven't seen existing Calamares code do that with the Config
* object, so just use the side effect of setting the variable, as
* done in existing code of Calamares modules.) */
config.runPartitionJobThenLeave = 1
});
timer.start();
}
function navNextFeature() {
var id;
/* Skip disabled features */
for (id = featureIdByScreen[screen] + 1; id < features.length; id++) {
/* First letter uppercase */
var name = features[id]["name"];
var nameUp = name.charAt(0).toUpperCase() + name.slice(1);
/* Check config.Feature<Name> */
var configOption = "feature" + nameUp;
if (config[configOption] === false) {
console.log("Skipping feature (disabled in config): " + name);
continue;
}
/* Check skipFeature<Name>() */
var funcName = "skipFeature" + nameUp;
if (eval("typeof " + funcName) === "function"
&& eval(funcName + "()")) {
console.log("Skipping feature (skip function): " + name);
continue;
}
break;
}
console.log("Navigating to feature: " + features[id]["name"]);
return navTo(features[id]["screens"][0]);
}
function navNext() {
var featureId = featureIdByScreen[screen];
var featureScreens = features[featureId]["screens"];
for (var i = 0; i<featureScreens.length; i++) {
/* Seek ahead until i is current screen */
if (featureScreens[i] != screen)
continue;
/* Navigate to next screen in same feature */
if (i + 1 < featureScreens.length) {
var screenNext = featureScreens[i + 1];
return navTo(screenNext);
}
/* Screen is last in feature */
return navNextFeature();
}
console.log("ERROR: navNext() failed for screen: " + screen);
}
function navBack() {
if (screenPrevious.length)
return navTo(screenPrevious.pop(), false);
ViewManager.back();
}
function onActivate() {
navTo(screen, false);
}
/* Input validation: show/clear failures */
function validationFailure(errorText, message="") {
errorText.text = message;
errorText.visible = true;
return false;
}
function validationFailureClear(errorText) {
errorText.text = "";
errorText.visible = false;
return true;
}
/* Input validation: user-screens (fde_pass, user_pass, ssh_credentials) */
function validatePin(userPin, userPinRepeat, errorText) {
var pin = userPin.text;
var repeat = userPinRepeat.text;
if (pin == "")
return validationFailure(errorText);
if (!pin.match(/^[0-9]*$/))
return validationFailure(errorText,
"Only digits are allowed.");
if (pin.length < 5)
return validationFailure(errorText,
"Too short: needs at least 5 digits.");
if (repeat == "")
return validationFailure(errorText);
if (repeat != pin)
return validationFailure(errorText,
"The PINs don't match.");
return validationFailureClear(errorText);
}
function validateUsername(username, errorText, extraReservedUsernames = []) {
var name = username.text;
var reserved = config.reservedUsernames.concat(extraReservedUsernames);
/* Validate characters */
for (var i = 0; i < name.length; i++) {
if (i) {
if (!name[i].match(/^[a-z0-9_-]$/))
return validationFailure(errorText,
"Characters must be lowercase" +
" letters, numbers,<br>" +
" underscores or minus signs.");
} else {
if (!name[i].match(/^[a-z_]$/))
return validationFailure(errorText,
"First character must be a" +
" lowercase letter or an" +
" underscore.");
}
}
/* Validate against reserved usernames */
for (var i = 0; i < reserved.length; i++) {
if (name == reserved[i])
return validationFailure(errorText, "Username '" +
reserved[i] +
"' is reserved.");
}
/* Passed */
return validationFailureClear(errorText);
}
function validateSshdUsername(username, errorText) {
return validateUsername(username, errorText, [config.username]);
}
function validateSshdPassword(password, passwordRepeat, errorText) {
var pass = password.text;
var repeat = passwordRepeat.text;
if (pass == "")
return validationFailure(errorText);
if (pass.length < 6)
return validationFailure(errorText,
"Too short: needs at least 6" +
" digits/characters.");
if (repeat == "")
return validationFailure(errorText);
if (pass != repeat)
return validationFailure(errorText, "Passwords don't match.");
return validationFailureClear(errorText);
}
function check_chars(input) {
for (var i = 0; i < input.length; i++) {
if (allowed_chars.indexOf(input[i]) == -1)
return false;
}
return true;
}
function allowed_chars_multiline() {
/* return allowed_chars split across multiple lines */
var step = 20;
var ret = "";
for (var i = 0; i < allowed_chars.length + step; i += step)
ret += allowed_chars.slice(i, i + step) + "\n";
return ret.trim();
}
function validatePassword(password, passwordRepeat, errorText) {
var pass = password.text;
var repeat = passwordRepeat.text;
if (pass == "")
return validationFailure(errorText);
/* This function gets called for the FDE password and for the user
* password. As of writing, all distributions shipping the mobile
* module are using osk-sdl to type in the FDE password after the
* installation, and another keyboard after booting up, to type in the
* user password. The osk-sdl password has the same keys as
* squeekboard's default layout, and other keyboards should be able to
* type these characters in as well. For now, verify that the password
* only contains characters that can be typed in by osk-sdl. If you
* need this to be more sophisticated, feel free to submit patches to
* make this more configurable. */
if (!check_chars(pass))
return validationFailure(errorText,
"The password must only contain" +
" these characters, others can possibly" +
" not be typed in after installation:\n" +
"\n" +
allowed_chars_multiline());
if (pass.length < 6)
return validationFailure(errorText,
"Too short: needs at least 6" +
" digits/characters.");
if (repeat == "")
return validationFailure(errorText);
if (pass != repeat)
return validationFailure(errorText, "Passwords don't match.");
return validationFailureClear(errorText);
}
}

21
modules/mobile/mobile.qrc Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file>mobile.qml</file>
<file>welcome.qml</file>
<file>install_target.qml</file> <!-- install from external to internal? -->
<file>install_target_confirm.qml</file> <!-- overwrite internal storage? -->
<file>user_pass.qml</file> <!-- default user: username, password -->
<file>ssh_confirm.qml</file> <!-- sshd: enable or not? -->
<file>ssh_credentials.qml</file> <!-- sshd user: username, password -->
<file>fs_selection.qml</file> <!-- filesystem selection -->
<file>fde_confirm.qml</file> <!-- enable FDE or not? -->
<file>fde_pass.qml</file> <!-- FDE password (optional) -->
<file>install_confirm.qml</file> <!-- final confirmation before install -->
<file>wait.qml</file> <!-- please wait while partitioning -->
</qresource>
</RCC>

View File

@@ -0,0 +1,68 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
Text {
id: mainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 30
wrapMode: Text.WordWrap
text: "If you don't know what SSH is, choose 'disable'.<br>" +
"<br>" +
"With 'enable', you will be asked for a second username and" +
" password. You will be able to login to the SSH server with" +
" these credentials via USB (172.16.42.1), Wi-Fi and possibly" +
" cellular network. It is recommended to replace the password" +
" with an SSH key after the installation.<br>" +
"<br>" +
"More information:<br>" +
"https://postmarketos.org/ssh"
width: 200
}
Button {
id: firstButton
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: mainText.bottom
anchors.topMargin: 40
width: 200
text: qsTr("Enable")
onClicked: {
config.isSshEnabled = true;
navNext();
}
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: firstButton.bottom
anchors.topMargin: 40
width: 200
text: qsTr("Disable")
onClicked: {
config.isSshEnabled = false;
navNextFeature();
}
}
}

View File

@@ -0,0 +1,107 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
TextField {
id: username
anchors.top: parent.top
placeholderText: qsTr("SSH username")
inputMethodHints: Qt.ImhPreferLowercase
onTextChanged: validateSshdUsername(username, errorTextUsername)
text: config.sshdUsername
onActiveFocusChanged: {
if(activeFocus) {
Qt.inputMethod.update(Qt.ImQueryInput);
}
}
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 50
width: 200
}
Text {
id: errorTextUsername
anchors.top: username.bottom
visible: false
wrapMode: Text.WordWrap
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 50
width: 200
}
TextField {
id: password
anchors.top: errorTextUsername.bottom
placeholderText: qsTr("SSH password")
echoMode: TextInput.Password
onTextChanged: validateSshdPassword(password, passwordRepeat,
errorTextPassword)
text: config.sshdPassword
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 50
width: 200
}
TextField {
id: passwordRepeat
anchors.top: password.bottom
placeholderText: qsTr("SSH password (repeat)")
echoMode: TextInput.Password
onTextChanged: validateSshdPassword(password, passwordRepeat,
errorTextPassword)
text: config.sshdPassword
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 50
width: 200
}
Text {
id: errorTextPassword
anchors.top: passwordRepeat.bottom
visible: false
wrapMode: Text.WordWrap
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 50
width: 200
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: errorTextPassword.bottom
anchors.topMargin: 40
width: 200
text: qsTr("Continue")
onClicked: {
if (validateSshdUsername(username, errorTextUsername) &&
validateSshdPassword(password, passwordRepeat,
errorTextPassword)) {
config.sshdUsername = username.text;
config.sshdPassword = password.text;
navNext();
}
}
}
}

View File

@@ -0,0 +1,143 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Item {
property var passPlaceholder: (config.userPasswordNumeric
? "PIN"
: "Password")
property var hints: (config.userPasswordNumeric
? Qt.ImhDigitsOnly
: Qt.ImhPreferLowercase)
property var validatePassFunc: (config.userPasswordNumeric
? validatePin
: validatePassword);
property var validateNameFunc: validateUsername;
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
Text {
id: usernameDescription
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
wrapMode: Text.WordWrap
text: (function() {
return "Set the username of your user. The default" +
" username is \"" + config.username + "\".";
}())
width: 200
}
TextField {
id: username
anchors.top: usernameDescription.bottom
placeholderText: qsTr("Username")
onTextChanged: validateNameFunc(username, errorText)
text: config.username
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 10
width: 200
}
Text {
id: userPassDescription
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: username.bottom
anchors.topMargin: 10
wrapMode: Text.WordWrap
text: (function() {
if (config.userPasswordNumeric) {
return "Set the numeric password of your user. The" +
" lockscreen will ask for this PIN. This is" +
" <i>not</i> the PIN of your SIM card. Make sure to" +
" remember it.";
} else {
return "Set the password of your user. The lockscreen will" +
" ask for this password. Make sure to remember it.";
}
}())
width: 200
}
TextField {
id: userPass
anchors.top: userPassDescription.bottom
placeholderText: qsTr(passPlaceholder)
echoMode: TextInput.Password
onTextChanged: validatePassFunc(userPass, userPassRepeat, errorText)
text: config.userPassword
/* Let the virtual keyboard change to digits only */
inputMethodHints: hints
onActiveFocusChanged: {
if(activeFocus) {
Qt.inputMethod.update(Qt.ImQueryInput)
}
}
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 10
width: 200
}
TextField {
id: userPassRepeat
anchors.top: userPass.bottom
placeholderText: qsTr(passPlaceholder + " (repeat)")
inputMethodHints: hints
echoMode: TextInput.Password
onTextChanged: validatePassFunc(userPass, userPassRepeat, errorText)
text: config.userPassword
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 10
width: 200
}
Text {
anchors.top: userPassRepeat.bottom
id: errorText
visible: false
wrapMode: Text.WordWrap
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 10
width: 200
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: errorText.bottom
anchors.topMargin: 10
width: 200
text: qsTr("Continue")
onClicked: {
if (validatePassFunc(userPass, userPassRepeat, errorText) && validateNameFunc(username, errorText)) {
config.userPassword = userPass.text;
config.username = username.text;
navNext();
}
}
}
}

45
modules/mobile/wait.qml Normal file
View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Page
{
id: fdeWait
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
width: parent.width
height: parent.height
Image {
id: logo
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
height: 50
fillMode: Image.PreserveAspectFit
source: "file:///usr/share/calamares/branding/default-mobile/logo.png"
}
Text {
id: waitText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: logo.bottom
anchors.topMargin: 50
wrapMode: Text.WordWrap
text: "Formatting and mounting target partition. This may" +
" take up to ten minutes, please be patient."
width: 200
}
}
}

View File

@@ -0,0 +1,65 @@
/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Page
{
id: welcome
Item {
id: appContainer
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
Item {
width: parent.width
height: parent.height
Image {
id: logo
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
height: 50
fillMode: Image.PreserveAspectFit
source: "file:///usr/share/calamares/branding/default-mobile/logo.png"
}
Text {
id: mainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: logo.bottom
anchors.topMargin: 10
horizontalAlignment: Text.AlignRight
text: "You are about to install<br>" +
"<b>" + config.osName +
" " + config.version + "</b><br>" +
"user interface " +
"<b>" + config.userInterface + "</b><br>" +
"architecture " +
"<b>" + config.arch + "</b><br>" +
"on your <br>" +
"<b>" + config.device + "</b><br>"
width: 200
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: mainText.bottom
anchors.topMargin: 10
width: 200
text: qsTr("Continue")
onClicked: navNext()
}
}
}
}

View File

@@ -0,0 +1,20 @@
# The OS-FreeBSD module does "all the things" in a FreeBSD installation.
# Since the other modules -- users, fstab, grub, pretty much all of them
# -- are Linux-specific, it doesn't make much sense to fork each of them
# or provide alternatives, so instead we have one module that completes
# a FreeBSD installation based on the GlobalStorage values set by
# Calamares viewmodules.
#
# SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: GPL-3.0-or-later
# License-Filename: LICENSE
#
calamares_add_plugin( os-freebsd
TYPE job
EXPORT_MACRO PLUGINDLLEXPORT_PRO
SOURCES
FreeBSDJob.cpp
SHARED_LIB
NO_CONFIG
)

View File

@@ -0,0 +1,58 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*/
#include "FreeBSDJob.h"
#include "CalamaresVersion.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/Logger.h"
#include <QDateTime>
#include <QProcess>
#include <QThread>
FreeBSDJob::FreeBSDJob( QObject* parent )
: Calamares::CppJob( parent )
{
}
FreeBSDJob::~FreeBSDJob() {}
QString
FreeBSDJob::prettyName() const
{
return tr( "FreeBSD Installation Job" );
}
Calamares::JobResult
FreeBSDJob::exec()
{
emit progress( 0.1 );
cDebug() << "[FREEBSD]";
Calamares::JobQueue::instance()->globalStorage()->debugDump();
emit progress( 0.5 );
QThread::sleep( 3 );
emit progress( 1.0 );
return Calamares::JobResult::ok();
}
void
FreeBSDJob::setConfigurationMap( const QVariantMap& configurationMap )
{
// TODO: actually fetch something from that configuration
m_configurationMap = configurationMap;
}
CALAMARES_PLUGIN_FACTORY_DEFINITION( FreeBSDJobFactory, registerPlugin< FreeBSDJob >(); )

View File

@@ -0,0 +1,39 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*/
#ifndef FREEBSDJOB_H
#define FREEBSDJOB_H
#include "CppJob.h"
#include "DllMacro.h"
#include "utils/PluginFactory.h"
#include <QObject>
#include <QVariantMap>
class PLUGINDLLEXPORT FreeBSDJob : public Calamares::CppJob
{
Q_OBJECT
public:
explicit FreeBSDJob( QObject* parent = nullptr );
virtual ~FreeBSDJob() override;
QString prettyName() const override;
Calamares::JobResult exec() override;
void setConfigurationMap( const QVariantMap& configurationMap ) override;
private:
QVariantMap m_configurationMap;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( FreeBSDJobFactory )
#endif // FREEBSDJOB_H

276
modules/os-nixos/main.py Normal file
View File

@@ -0,0 +1,276 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# === This file is part of Calamares - <https://github.com/calamares> ===
#
# Calamares is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Calamares is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Calamares. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: GPL-3.0-or-later
#
"""
=== NixOS Configuration
NixOS has its own "do all the things" configuration file which
declaratively handles what things need to be done in the target
system, and it has an existing tool to "execute" that declarative
specification. This module takes configuration values set by
Calamares viewmodules (e.g. the users module) and puts
them into the configuration file in the target system,
and then runs the necessary NixOS specific tools.
"""
import libcalamares
import os
from time import gmtime, strftime, sleep
import gettext
_ = gettext.translation("calamares-python",
localedir=libcalamares.utils.gettext_path(),
languages=libcalamares.utils.gettext_languages(),
fallback=True).gettext
# The following long **long** string is the entire text of
# a nix-configuration file. It is cribbed from, and adapted from,
# the sample file in https://github.com/itamar567/dotnix .
#
# We are going to substitute values into this text. However,
# Python's .format() function wants parens { } around variable
# names, and Nix's config file wants to use parens { } for block
# structure. So we have a compromise format here:
#
# - Write the config file as you would normally,
# - Write @@variable@@ instead of {variable}
#
# Some minor trickery later will massage this and substitute variables.
#
configuration_nix_sample = """# Nix configuration file
{ config, pkgs, ... }:
{
imports =
[ # Include the results of the hardware scan.
./hardware-configuration.nix
./command-not-found/command-not-found.nix
];
# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# Use Zen Kernel
boot.kernelPackages = pkgs.linuxPackages_zen;
networking.hostName = "@@hostname@@"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Set your time zone.
time.timeZone = "@@timezone@@";
# The global useDHCP flag is deprecated, therefore explicitly set to false here.
networking.useDHCP = false;
networking.interfaces.enp42s0.useDHCP = true;
# Select internationalisation properties.
i18n.defaultLocale = "en_US.UTF-8";
# Configure X11
services.xserver = {
enable = true;
windowManager.i3 = {
enable = true;
package = pkgs.i3-gaps;
};
# Set i3 to the default session in the display manager
displayManager.defaultSession = "none+i3";
};
# SSH fix
programs.ssh.askPassword = pkgs.lib.mkForce "";
# Enable CUPS to print documents.
services.printing.enable = true;
# Enable sound.
sound.enable = true;
hardware.pulseaudio.enable = true;
# Define a user account. Don't forget to set a password with passwd.
users.users.username = {
isNormalUser = true;
extraGroups = [ "wheel" "libvirtd" ];
};
# Disable password for sudo
security.sudo.extraRules= [{
groups = [ "wheel" ];
commands = [{
command = "ALL" ;
options= [ "NOPASSWD" ];
}];
}];
# Set ZSH as the default shell
users.defaultUserShell = pkgs.zsh;
# clean /tmp on boot
boot.cleanTmpDir=true;
# Config packages
nixpkgs.config = {
allowUnfree = true;
chromium = {
enableWideVine = true;
};
};
# Automatically upgrade the system
# This service is a modified version of https://github.com/NixOS/nixpkgs/blob/nixos-21.05/nixos/modules/tasks/auto-upgrade.nix#L122
systemd = {
services.nixos-upgrade = {
description = "NixOS Upgrade";
# We use --upgrade, so we need internet access
wants = [ "network-online.target" ];
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig.Type = "oneshot";
environment = config.nix.envVars // {
inherit (config.environment.sessionVariables) NIX_PATH;
HOME = "/root";
} // config.networking.proxy.envVars;
path = with pkgs; [
coreutils
gnutar
xz.bin
gzip
gitMinimal
config.nix.package.out
];
script = let
nixos-rebuild =
"${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
in ''
${nixos-rebuild} boot --upgrade
booted="$(readlink /run/booted-system/{initrd,kernel,kernel-modules})"
built="$(readlink /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})"
if [ "$booted" = "$built" ]; then
${nixos-rebuild} switch
fi
'';
};
# To start the service at boot, we will use a systemd timer
timers.nixos-upgrade = {
wantedBy = [ "timers.target" ];
partOf = [ "nixos-upgrade.service" ];
timerConfig.OnBootSec = "5s";
};
};
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
programs.mtr.enable = true;
programs.gnupg.agent = {
enable = true;
enableSSHSupport = true;
};
# Enable the OpenSSH daemon.
services.openssh.enable = true;
# Open ports in the firewall.
networking.firewall.enable = false;
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. Its perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "20.09"; # Did you read the comment?
}
"""
def pretty_name():
return _("NixOS Configuration.")
def catenate(d, key, *values):
"""
Sets @p d[key] to the string-concatenation of @p values
if none of the values are None.
This can be used to set keys conditionally based on
the values being found.
"""
if [v for v in values if v is None]:
return
d[key] = "".join(values)
def run():
"""NixOS Configuration."""
gs = libcalamares.globalstorage
text = configuration_nix_sample
# Collect variables to substitute into the main text
variables = dict()
catenate(variables, "hostname", gs.value("hostname"))
catenate(variables, "timezone", gs.value("locationRegion"), "/", gs.value("locationZone"))
# Check that all variables are used
for key in variables.keys():
pattern = "@@{key}@@".format(key=key)
if not pattern in text:
libcalamares.utils.warning("Variable '{key}' is not used.".format(key=key))
# Check that all patterns exist
import re
variable_pattern = re.compile("@@\w+@@")
for match in variable_pattern.finditer(text):
variable_name = text[match.start()+2:match.end()-2]
if not variable_name in variables:
libcalamares.utils.warning("Variable '{key}' is used but not defined.".format(key=variable_name))
# Do the substitutions
for key in variables.keys():
pattern = "@@{key}@@".format(key=key)
text = text.replace(pattern, str(variables[key]))
# Write the result to a temp-file, then run the main tool.
# There is no progress reporting from the tool, so it's going
# to seem like the module is hanging (see issue #1740).
configuration_filename = "/tmp/configuration.nix"
with open(configuration_filename, "w") as f:
f.write(text)
libcalamares.job.setprogress(0.1)
libcalamares.utils.check_target_env_call(["nix", configuration_filename])
# To indicate an error, return a tuple of:
# (message, detailed-error-message)
return None

View File

@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
#
# The NixOS module writes a nix-configuration file and then calls
# the Nix configuration tool to do the actual work of building
# the target system.
---
type: "job"
name: "os-nixos"
interface: "python"
script: "main.py"

View File

@@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
---
hostname: my-nix-host
locationRegion: Asia
locationZone: Kolkata

View File

@@ -10,9 +10,9 @@ set(_extra_src "")
### OPTIONAL AppData XML support in PackageModel
#
#
option(BUILD_APPDATA "Support appdata: items in PackageChooser (requires QtXml)" ON)
option(BUILD_APPDATA "Support appdata: items in PackageChooser (requires QtXml)" OFF)
if(BUILD_APPDATA)
find_package(${qtname} COMPONENTS Xml)
find_package(${qtname} REQUIRED COMPONENTS Xml)
if(TARGET ${qtname}::Xml)
add_definitions(-DHAVE_APPDATA)
list(APPEND _extra_libraries ${qtname}::Xml)
@@ -23,24 +23,8 @@ endif()
### OPTIONAL AppStream support in PackageModel
#
#
option(BUILD_APPSTREAM "Support appstream: items in PackageChooser (requires libappstream-qt)" ON)
if(BUILD_APPSTREAM)
find_package(AppStreamQt)
set_package_properties(
AppStreamQt
PROPERTIES
DESCRIPTION "Support for AppStream (cache) data"
URL "https://github.com/ximion/appstream"
PURPOSE "AppStream provides package data"
TYPE OPTIONAL
)
if(AppStreamQt_FOUND)
add_definitions(-DHAVE_APPSTREAM_VERSION=${AppStreamQt_VERSION_MAJOR})
list(APPEND _extra_libraries AppStreamQt)
list(APPEND _extra_src ItemAppStream.cpp)
endif()
endif()
# include(AppStreamHelper)
#
calamares_add_plugin(packagechooser
TYPE viewmodule
EXPORT_MACRO PLUGINDLLEXPORT_PRO
@@ -49,6 +33,8 @@ calamares_add_plugin(packagechooser
PackageChooserPage.cpp
PackageChooserViewStep.cpp
PackageModel.cpp
LoaderQueue.cpp
PackageTreeItem.cpp
${_extra_src}
RESOURCES
packagechooser.qrc
@@ -59,7 +45,11 @@ calamares_add_plugin(packagechooser
SHARED_LIB
)
# include(CalamaresAddTest)
# if(AppStreamQt_FOUND)
# target_link_libraries(calamares_viewmodule_packagechooser PRIVATE calamares::appstreamqt)
# target_sources(calamares_viewmodule_packagechooser PRIVATE ItemAppStream.cpp)
# endif()
# calamares_add_test(
# packagechoosertest
# GUI

View File

@@ -10,13 +10,14 @@
#include "Config.h"
#include "LoaderQueue.h"
#ifdef HAVE_APPDATA
#include "ItemAppData.h"
#endif
#ifdef HAVE_APPSTREAM_VERSION
#include "ItemAppStream.h"
#include <AppStreamQt/pool.h>
#include <memory>
#endif
@@ -26,6 +27,9 @@
#include "packages/Globals.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include "network/Manager.h"
#include <QNetworkReply>
/** @brief This removes any values from @p groups that match @p source
*
@@ -87,12 +91,68 @@ PackageChooserMethodNames()
Config::Config( QObject* parent )
: Calamares::ModuleSystem::Config( parent )
, m_model( new PackageListModel( this ) )
, m_netmodel( new PackageNetModel( this ) )
, m_mode( PackageChooserMode::Required )
{
}
Config::~Config() {}
QString
Config::status() const
{
switch ( m_status )
{
case Status::Ok:
return QString();
case Status::FailedBadConfiguration:
return tr( "Network Installation. (Disabled: Incorrect configuration)" );
case Status::FailedBadData:
return tr( "Network Installation. (Disabled: Received invalid groups data)" );
case Status::FailedInternalError:
return tr( "Network Installation. (Disabled: Internal error)" );
case Status::FailedNetworkError:
return tr( "Network Installation. (Disabled: Unable to fetch package lists, check your network connection)" );
case Status::FailedNoData:
return tr( "Network Installation. (Disabled: No package list)" );
}
__builtin_unreachable();
}
void
Config::setStatus( Status s )
{
m_status = s;
emit statusChanged( status() );
}
void
Config::loadGroupList( const QVariantList& groupData )
{
m_netmodel->setupModelData( groupData );
if ( m_netmodel->rowCount() < 1 )
{
cWarning() << "NetInstall groups data was empty.";
setStatus( Status::FailedNoData );
}
else
{
setStatus( Status::Ok );
}
}
void
Config::loadingDone()
{
if ( m_queue )
{
m_queue->deleteLater();
m_queue = nullptr;
}
emit statusReady();
}
const PackageItem&
Config::introductionPackage() const
{
@@ -324,6 +384,29 @@ Config::setConfigurationMap( const QVariantMap& configurationMap )
{
fillModel( m_model, configurationMap.value( "items" ).toList() );
// Lastly, load the groups data
// const QString key = QStringLiteral( "groupsUrl" );
//
//
// const auto& groupsUrlVariant = configurationMap.value( key );
// m_queue = new LoaderQueue( this );
// if ( Calamares::typeOf( groupsUrlVariant ) == Calamares::StringVariantType )
// {
// m_queue->append( SourceItem::makeSourceItem( groupsUrlVariant.toString(), configurationMap ) );
// }
// else if ( Calamares::typeOf( groupsUrlVariant ) == Calamares::ListVariantType )
// {
// for ( const auto& s : groupsUrlVariant.toStringList() )
// {
// m_queue->append( SourceItem::makeSourceItem( s, configurationMap ) );
// }
// }
//
// setStatus( required() ? Status::FailedNoData : Status::Ok );
// cDebug() << "Loading netinstall from" << m_queue->count() << "alternate sources.";
// connect( m_queue, &LoaderQueue::done, this, &Config::loadingDone );
// m_queue->load();
QString default_item_id = Calamares::getString( configurationMap, "default" );
if ( !default_item_id.isEmpty() )
{

View File

@@ -16,9 +16,16 @@
#include "modulesystem/Config.h"
#include "modulesystem/InstanceKey.h"
#include <QObject>
#include <QVariantMap>
#include <memory>
#include <optional>
class LoaderQueue;
enum class PackageChooserMode
{
Optional, // zero or one
@@ -55,6 +62,9 @@ class Config : public Calamares::ModuleSystem::Config
Q_PROPERTY( QString packageChoice READ packageChoice WRITE setPackageChoice NOTIFY packageChoiceChanged )
Q_PROPERTY( QString prettyStatus READ prettyStatus NOTIFY prettyStatusChanged FINAL )
Q_PROPERTY( PackageNetModel* PackageNetModel MEMBER m_netmodel FINAL )
Q_PROPERTY( QString status READ status NOTIFY statusChanged FINAL )
public:
Config( QObject* parent = nullptr );
~Config() override;
@@ -73,6 +83,10 @@ public:
PackageListModel* model() const { return m_model; }
QModelIndex defaultSelectionIndex() const { return m_defaultModelIndex; }
PackageNetModel* netmodel() const { return m_netmodel; }
void loadGroupList( const QVariantList& groupData );
/** @brief Returns an "introductory package" which describes packagechooser
*
* If the model contains a "none" package, returns that one on
@@ -101,14 +115,43 @@ public:
QString prettyName() const;
QString prettyStatus() const;
enum class Status
{
Ok,
FailedBadConfiguration,
FailedInternalError,
FailedNetworkError,
FailedBadData,
FailedNoData
};
/// Human-readable, translated representation of the status
QString status() const;
/// Internal code for the status
Status statusCode() const { return m_status; }
void setStatus( Status s );
signals:
void packageChoiceChanged( QString packageChoice );
void prettyStatusChanged();
Q_SIGNALS:
void statusChanged( QString status ); ///< Something changed
void sidebarLabelChanged( QString label );
void titleLabelChanged( QString label );
void statusReady(); ///< Loading groups is complete
private Q_SLOTS:
void loadingDone();
private:
PackageListModel* m_model = nullptr;
QModelIndex m_defaultModelIndex;
PackageNetModel* m_netmodel = nullptr;
LoaderQueue* m_queue = nullptr;
Status m_status = Status::Ok;
/// Selection mode for this module
PackageChooserMode m_mode = PackageChooserMode::Optional;
/// How this module stores to GS

View File

@@ -7,20 +7,16 @@
*
*/
/** @brief Loading items from AppData XML files.
/** @brief Loading items from AppStream database.
*
* Only used if QtXML is found, implements PackageItem::fromAppData().
* Only used if AppStreamQt is found, implements PackageItem::fromAppStream().
*/
#include "PackageModel.h"
#include "ItemAppStream.h"
#include "locale/TranslationsModel.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include <AppStreamQt/image.h>
#include <AppStreamQt/pool.h>
#include <AppStreamQt/screenshot.h>
/// @brief Return number of pixels in a size, for < ordering purposes
static inline quint64
sizeOrder( const QSize& size )

View File

@@ -12,10 +12,28 @@
#include "PackageModel.h"
namespace AppStream
{
class Pool;
} // namespace AppStream
/*
* This weird include mechanism is because an #include line is allowed
* to consist of preprocessor-tokens, which are expanded, and then
* the #include is *re*processed. But if it starts with < or ", then
* preprocessor tokens are not expanded. So we build up a #include <>
* style line with a suitable path -- if we are given a value for
* HAVE_APPSTREAM_HEADERS, that is the directory that the AppStreamQt
* headers live in.
*/
#define CALAMARES_LT <
#define CALAMARES_GT >
#ifndef HAVE_APPSTREAM_HEADERS
#define HAVE_APPSTREAM_HEADERS AppStreamQt
#endif
#include CALAMARES_LT HAVE_APPSTREAM_HEADERS/pool.h CALAMARES_GT
#include CALAMARES_LT HAVE_APPSTREAM_HEADERS/image.h CALAMARES_GT
#include CALAMARES_LT HAVE_APPSTREAM_HEADERS/screenshot.h CALAMARES_GT
#undef CALAMARES_LT
#undef CALAMARES_GT
/** @brief Loads an item from AppStream data.
*

View File

@@ -0,0 +1,205 @@
/*
* SPDX-FileCopyrightText: 2016 Luca Giambonini <almack@chakraos.org>
* SPDX-FileCopyrightText: 2016 Lisa Vitolo <shainer@chakraos.org>
* SPDX-FileCopyrightText: 2017 Kyle Robbertze <krobbertze@gmail.com>
* SPDX-FileCopyrightText: 2017-2018 2020, Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "LoaderQueue.h"
#include "Config.h"
#include "network/Manager.h"
#include "utils/Logger.h"
#include "utils/RAII.h"
#include "utils/Yaml.h"
#include <QNetworkReply>
#include <QTimer>
/** @brief Call fetchNext() on the queue if it can
*
* On destruction, a new call to fetchNext() is queued, so that
* the queue continues loading. Calling release() before the
* destructor skips the fetchNext(), ending the queue-loading.
*
* Calling done(b) is a conditional release: if @p b is @c true,
* queues a call to done() on the queue and releases it; otherwise,
* does nothing.
*/
class FetchNextUnless
{
public:
FetchNextUnless( LoaderQueue* q )
: m_q( q )
{
}
~FetchNextUnless()
{
if ( m_q )
{
QMetaObject::invokeMethod( m_q, "fetchNext", Qt::QueuedConnection );
}
}
void release() { m_q = nullptr; }
void done( bool b )
{
if ( b )
{
if ( m_q )
{
QMetaObject::invokeMethod( m_q, "done", Qt::QueuedConnection );
}
release();
}
}
private:
LoaderQueue* m_q = nullptr;
};
SourceItem
SourceItem::makeSourceItem( const QString& groupsUrl, const QVariantMap& configurationMap )
{
if ( groupsUrl == QStringLiteral( "local" ) )
{
return SourceItem { QUrl(), configurationMap.value( "groups" ).toList() };
}
else
{
return SourceItem { QUrl { groupsUrl }, QVariantList() };
}
}
LoaderQueue::LoaderQueue( Config* parent )
: QObject( parent )
, m_config( parent )
{
}
void
LoaderQueue::append( SourceItem&& i )
{
m_queue.append( std::move( i ) );
}
void
LoaderQueue::load()
{
QMetaObject::invokeMethod( this, "fetchNext", Qt::QueuedConnection );
}
void
LoaderQueue::fetchNext()
{
if ( m_queue.isEmpty() )
{
emit done();
return;
}
auto source = m_queue.takeFirst();
if ( source.isLocal() )
{
m_config->loadGroupList( source.data );
emit done();
}
else
{
fetch( source.url );
}
}
void
LoaderQueue::fetch( const QUrl& url )
{
FetchNextUnless next( this );
if ( !url.isValid() )
{
m_config->setStatus( Config::Status::FailedBadConfiguration );
cDebug() << "Invalid URL" << url;
return;
}
using namespace Calamares::Network;
cDebug() << "NetInstall loading groups from" << url;
QNetworkReply* reply = Manager().asynchronousGet(
url,
RequestOptions( RequestOptions::FakeUserAgent | RequestOptions::FollowRedirect, std::chrono::seconds( 30 ) ) );
if ( !reply )
{
cDebug() << Logger::SubEntry << "Request failed immediately.";
// If nobody sets a different status, this will remain
m_config->setStatus( Config::Status::FailedBadConfiguration );
}
else
{
// When the network request is done, **then** we might
// do the next item from the queue, so don't call fetchNext() now.
next.release();
m_reply = reply;
connect( reply, &QNetworkReply::finished, this, &LoaderQueue::dataArrived );
}
}
void
LoaderQueue::dataArrived()
{
FetchNextUnless next( this );
if ( !m_reply || !m_reply->isFinished() )
{
cWarning() << "NetInstall data called too early.";
m_config->setStatus( Config::Status::FailedInternalError );
return;
}
cDebug() << "NetInstall group data received" << m_reply->size() << "bytes from" << m_reply->url();
cqDeleter< QNetworkReply > d { m_reply };
// If m_required is *false* then we still say we're ready
// even if the reply is corrupt or missing.
if ( m_reply->error() != QNetworkReply::NoError )
{
cWarning() << "unable to fetch netinstall package lists.";
cDebug() << Logger::SubEntry << "Netinstall reply error: " << m_reply->error();
cDebug() << Logger::SubEntry << "Request for url: " << m_reply->url().toString()
<< " failed with: " << m_reply->errorString();
m_config->setStatus( Config::Status::FailedNetworkError );
return;
}
QByteArray yamlData = m_reply->readAll();
try
{
auto groups = ::YAML::Load( yamlData.constData() );
if ( groups.IsSequence() )
{
m_config->loadGroupList( Calamares::YAML::sequenceToVariant( groups ) );
next.done( m_config->statusCode() == Config::Status::Ok );
}
else if ( groups.IsMap() )
{
auto map = Calamares::YAML::mapToVariant( groups );
m_config->loadGroupList( map.value( "groups" ).toList() );
next.done( m_config->statusCode() == Config::Status::Ok );
}
else
{
cWarning() << "NetInstall groups data does not form a sequence.";
}
}
catch ( ::YAML::Exception& e )
{
Calamares::YAML::explainException( e, yamlData, "netinstall groups data" );
m_config->setStatus( Config::Status::FailedBadData );
}
}

View File

@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2016 Luca Giambonini <almack@chakraos.org>
* SPDX-FileCopyrightText: 2016 Lisa Vitolo <shainer@chakraos.org>
* SPDX-FileCopyrightText: 2017 Kyle Robbertze <krobbertze@gmail.com>
* SPDX-FileCopyrightText: 2017-2018 2020, Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef NETINSTALL_LOADERQUEUE_H
#define NETINSTALL_LOADERQUEUE_H
#include <QQueue>
#include <QUrl>
#include <QVariantList>
class Config;
class QNetworkReply;
/** @brief Data about an entry in *groupsUrl*
*
* This can be a specific URL, or "local" which uses data stored
* in the configuration file itself.
*/
struct SourceItem
{
QUrl url;
QVariantList data;
bool isUrl() const { return url.isValid(); }
bool isLocal() const { return !data.isEmpty(); }
bool isValid() const { return isUrl() || isLocal(); }
/** @brief Create a SourceItem
*
* If the @p groupsUrl is @c "local" then the *groups* key in
* the @p configurationMap is used as the source; otherwise the
* string is used as an actual URL.
*/
static SourceItem makeSourceItem( const QString& groupsUrl, const QVariantMap& configurationMap );
};
/** @brief Queue of source items to load
*
* Queue things up by calling append() and then kick things off
* by calling load(). This will try to load the items, in order;
* the first one that succeeds will end the loading process.
*
* Signal done() is emitted when done (also when all of the items fail).
*/
class LoaderQueue : public QObject
{
Q_OBJECT
public:
LoaderQueue( Config* parent );
void append( SourceItem&& i );
int count() const { return m_queue.count(); }
public Q_SLOTS:
void load();
void fetchNext();
void fetch( const QUrl& url );
void dataArrived();
Q_SIGNALS:
void done();
private:
QQueue< SourceItem > m_queue;
Config* m_config = nullptr;
QNetworkReply* m_reply = nullptr;
};
#endif

View File

@@ -13,6 +13,9 @@
#include "utils/Logger.h"
#include "utils/Variant.h"
#include "compat/Variant.h"
#include "utils/Yaml.h"
#include <QFileInfo>
/** @brief A wrapper for Calamares::getSubMap that excludes the success param
@@ -194,3 +197,379 @@ PackageListModel::data( const QModelIndex& index, int role ) const
return QVariant();
}
/// Recursive helper for setSelections()
static void
setSelections( const QStringList& selectNames, PackageTreeItem* item )
{
for ( int i = 0; i < item->childCount(); i++ )
{
auto* child = item->child( i );
setSelections( selectNames, child );
}
if ( item->isGroup() && selectNames.contains( item->name() ) )
{
item->setSelected( Qt::CheckState::Checked );
}
}
/** @brief Collects all the "source" values from @p groupList
*
* Iterates over @p groupList and returns all nonempty "source"
* values from the maps.
*
*/
static QStringList
collectSources( const QVariantList& groupList )
{
QStringList sources;
for ( const QVariant& group : groupList )
{
QVariantMap groupMap = group.toMap();
if ( !groupMap[ "source" ].toString().isEmpty() )
{
sources.append( groupMap[ "source" ].toString() );
}
}
return sources;
}
PackageNetModel::PackageNetModel( QObject* parent )
: QAbstractItemModel( parent )
{
}
PackageNetModel::~PackageNetModel()
{
delete m_rootItem;
}
QModelIndex
PackageNetModel::index( int row, int column, const QModelIndex& parent ) const
{
if ( !m_rootItem || !hasIndex( row, column, parent ) )
{
return QModelIndex();
}
PackageTreeItem* parentItem;
if ( !parent.isValid() )
{
parentItem = m_rootItem;
}
else
{
parentItem = static_cast< PackageTreeItem* >( parent.internalPointer() );
}
PackageTreeItem* childItem = parentItem->child( row );
if ( childItem )
{
return createIndex( row, column, childItem );
}
else
{
return QModelIndex();
}
}
QModelIndex
PackageNetModel::parent( const QModelIndex& index ) const
{
if ( !m_rootItem || !index.isValid() )
{
return QModelIndex();
}
PackageTreeItem* child = static_cast< PackageTreeItem* >( index.internalPointer() );
PackageTreeItem* parent = child->parentItem();
if ( parent == m_rootItem )
{
return QModelIndex();
}
return createIndex( parent->row(), 0, parent );
}
int
PackageNetModel::rowCount( const QModelIndex& parent ) const
{
if ( !m_rootItem || ( parent.column() > 0 ) )
{
return 0;
}
PackageTreeItem* parentItem;
if ( !parent.isValid() )
{
parentItem = m_rootItem;
}
else
{
parentItem = static_cast< PackageTreeItem* >( parent.internalPointer() );
}
return parentItem->childCount();
}
int
PackageNetModel::columnCount( const QModelIndex& ) const
{
return 2;
}
QVariant
PackageNetModel::data( const QModelIndex& index, int role ) const
{
if ( !m_rootItem || !index.isValid() )
{
return QVariant();
}
PackageTreeItem* item = static_cast< PackageTreeItem* >( index.internalPointer() );
switch ( role )
{
case Qt::CheckStateRole:
return index.column() == NameColumn ? ( item->isImmutable() ? QVariant() : item->isSelected() ) : QVariant();
case Qt::DisplayRole:
return item->isHidden() ? QVariant() : item->data( index.column() );
case MetaExpandRole:
return item->isHidden() ? false : item->expandOnStart();
default:
return QVariant();
}
}
bool
PackageNetModel::setData( const QModelIndex& index, const QVariant& value, int role )
{
if ( !m_rootItem )
{
return false;
}
if ( role == Qt::CheckStateRole && index.isValid() )
{
PackageTreeItem* item = static_cast< PackageTreeItem* >( index.internalPointer() );
item->setSelected( static_cast< Qt::CheckState >( value.toInt() ) );
emit dataChanged( this->index( 0, 0 ),
index.sibling( index.column(), index.row() + 1 ),
QVector< int >( Qt::CheckStateRole ) );
}
return true;
}
Qt::ItemFlags
PackageNetModel::flags( const QModelIndex& index ) const
{
if ( !m_rootItem || !index.isValid() )
{
return Qt::ItemFlags();
}
if ( index.column() == NameColumn )
{
PackageTreeItem* item = static_cast< PackageTreeItem* >( index.internalPointer() );
if ( item->isImmutable() || item->isNoncheckable() )
{
return QAbstractItemModel::flags( index ); //Qt::NoItemFlags;
}
return Qt::ItemIsUserCheckable | QAbstractItemModel::flags( index );
}
return QAbstractItemModel::flags( index );
}
QVariant
PackageNetModel::headerData( int section, Qt::Orientation orientation, int role ) const
{
if ( orientation == Qt::Horizontal && role == Qt::DisplayRole )
{
return ( section == NameColumn ) ? tr( "Name" ) : tr( "Description" );
}
return QVariant();
}
void
PackageNetModel::setSelections( const QStringList& selectNames )
{
if ( m_rootItem )
{
::setSelections( selectNames, m_rootItem );
}
}
PackageTreeItem::List
PackageNetModel::getPackages() const
{
if ( !m_rootItem )
{
return PackageTreeItem::List();
}
auto items = getItemPackages( m_rootItem );
for ( auto package : m_hiddenItems )
{
if ( package->hiddenSelected() )
{
items.append( getItemPackages( package ) );
}
}
return items;
}
PackageTreeItem::List
PackageNetModel::getItemPackages( PackageTreeItem* item ) const
{
PackageTreeItem::List selectedPackages;
for ( int i = 0; i < item->childCount(); i++ )
{
auto* child = item->child( i );
if ( child->isSelected() == Qt::Unchecked )
{
continue;
}
if ( child->isPackage() ) // package
{
selectedPackages.append( child );
}
else
{
selectedPackages.append( getItemPackages( child ) );
}
}
return selectedPackages;
}
void
PackageNetModel::setupModelData( const QVariantList& groupList, PackageTreeItem* parent )
{
for ( const auto& group : groupList )
{
QVariantMap groupMap = group.toMap();
if ( groupMap.isEmpty() )
{
continue;
}
PackageTreeItem* item = new PackageTreeItem( groupMap, PackageTreeItem::GroupTag { parent } );
if ( groupMap.contains( "selected" ) )
{
item->setSelected( Calamares::getBool( groupMap, "selected", false ) ? Qt::Checked : Qt::Unchecked );
}
if ( groupMap.contains( "packages" ) )
{
for ( const auto& packageName : groupMap.value( "packages" ).toList() )
{
if ( Calamares::typeOf( packageName ) == Calamares::StringVariantType )
{
item->appendChild( new PackageTreeItem( packageName.toString(), item ) );
}
else
{
QVariantMap m = packageName.toMap();
if ( !m.isEmpty() )
{
item->appendChild( new PackageTreeItem( m, PackageTreeItem::PackageTag { item } ) );
}
}
}
if ( !item->childCount() )
{
cWarning() << "*packages* under" << item->name() << "is empty.";
}
}
if ( groupMap.contains( "subgroups" ) )
{
bool haveWarned = false;
const auto& subgroupValue = groupMap.value( "subgroups" );
if ( !subgroupValue.canConvert< QVariantList >() )
{
cWarning() << "*subgroups* under" << item->name() << "is not a list.";
haveWarned = true;
}
QVariantList subgroups = groupMap.value( "subgroups" ).toList();
if ( !subgroups.isEmpty() )
{
setupModelData( subgroups, item );
// The children might be checked while the parent isn't (yet).
// Children are added to their parent (below) without affecting
// the checked-state -- do it manually. Items with subgroups
// but no children have only hidden children -- those get
// handled specially.
if ( item->childCount() > 0 )
{
item->updateSelected();
}
}
else
{
if ( !haveWarned )
{
cWarning() << "*subgroups* list under" << item->name() << "is empty.";
}
}
}
if ( item->isHidden() )
{
m_hiddenItems.append( item );
if ( !item->isSelected() )
{
cWarning() << "Item" << ( item->parentItem() ? item->parentItem()->name() : QString() ) << '.'
<< item->name() << "is hidden, but not selected.";
}
}
else
{
item->setCheckable( true );
parent->appendChild( item );
}
}
}
void
PackageNetModel::setupModelData( const QVariantList& l )
{
beginResetModel();
delete m_rootItem;
m_rootItem = new PackageTreeItem();
setupModelData( l, m_rootItem );
endResetModel();
}
void
PackageNetModel::appendModelData( const QVariantList& groupList )
{
if ( m_rootItem )
{
beginResetModel();
const QStringList sources = collectSources( groupList );
if ( !sources.isEmpty() )
{
// Prune any existing data from the same source
QList< int > removeList;
for ( int i = 0; i < m_rootItem->childCount(); i++ )
{
PackageTreeItem* child = m_rootItem->child( i );
if ( sources.contains( child->source() ) )
{
removeList.insert( 0, i );
}
}
for ( const int& item : qAsConst( removeList ) )
{
m_rootItem->removeChild( item );
}
}
// Add the new data to the model
setupModelData( groupList, m_rootItem );
endResetModel();
}
}

View File

@@ -13,6 +13,11 @@
#include "locale/TranslatableConfiguration.h"
#include "utils/NamedEnum.h"
#include "PackageTreeItem.h"
#include <QAbstractItemModel>
#include <QString>
#include <QAbstractListModel>
#include <QObject>
#include <QPixmap>
@@ -132,4 +137,78 @@ private:
PackageList m_packages;
};
namespace YAML
{
class Node;
} // namespace YAML
class PackageNetModel : public QAbstractItemModel
{
Q_OBJECT
public:
// Names for columns (unused in the code)
static constexpr const int NameColumn = 0;
static constexpr const int DescriptionColumn = 1;
/* The only interesting roles are DisplayRole (with text depending
* on the column, and MetaExpandRole which tells if an index
* should be initially expanded.
*/
static constexpr const int MetaExpandRole = Qt::UserRole + 1;
explicit PackageNetModel( QObject* parent = nullptr );
~PackageNetModel() override;
void setupModelData( const QVariantList& l );
QVariant data( const QModelIndex& index, int role ) const override;
bool setData( const QModelIndex& index, const QVariant& value, int role = Qt::EditRole ) override;
Qt::ItemFlags flags( const QModelIndex& index ) const override;
QModelIndex index( int row, int column, const QModelIndex& parent = QModelIndex() ) const override;
QModelIndex parent( const QModelIndex& index ) const override;
QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override;
int rowCount( const QModelIndex& parent = QModelIndex() ) const override;
int columnCount( const QModelIndex& parent = QModelIndex() ) const override;
/** @brief Sets the checked flag on matching groups in the tree
*
* Recursively traverses the tree pointed to by m_rootItem and
* checks if a group name matches any of the items in @p selectNames.
* If a match is found, set check the box for that group and it's children.
*
* Individual packages will not be matched.
*
*/
void setSelections( const QStringList& selectNames );
PackageTreeItem::List getPackages() const;
PackageTreeItem::List getItemPackages( PackageTreeItem* item ) const;
/** @brief Appends groups to the tree
*
* Uses the data from @p groupList to add elements to the
* existing tree that m_rootItem points to. If m_rootItem
* is not valid, it does nothing
*
* Before adding anything to the model, it ensures that there
* is no existing data from the same source. If there is, that
* data is pruned first
*
*/
void appendModelData( const QVariantList& groupList );
private:
friend class ItemTests;
void setupModelData( const QVariantList& l, PackageTreeItem* parent );
PackageTreeItem* m_rootItem = nullptr;
PackageTreeItem::List m_hiddenItems;
};
#endif

View File

@@ -0,0 +1,311 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2017 Kyle Robbertze <kyle@aims.ac.za>
* SPDX-FileCopyrightText: 2017 2020, Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "PackageTreeItem.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
/** @brief Should a package be selected, given its parent's state? */
static Qt::CheckState
parentCheckState( PackageTreeItem* parent )
{
if ( parent )
{
// Avoid partially-checked .. a package can't be partial
return parent->isSelected() == Qt::Unchecked ? Qt::Unchecked : Qt::Checked;
}
else
{
return Qt::Unchecked;
}
}
/** @brief Should a subgroup be marked critical?
*
* If set explicitly, then use that, otherwise use the parent's critical-ness.
*/
static bool
parentCriticality( const QVariantMap& groupData, PackageTreeItem* parent )
{
if ( groupData.contains( "critical" ) )
{
return Calamares::getBool( groupData, "critical", false );
}
return parent ? parent->isCritical() : false;
}
PackageTreeItem::PackageTreeItem( const QString& packageName, PackageTreeItem* parent )
: m_parentItem( parent )
, m_packageName( packageName )
, m_selected( parentCheckState( parent ) )
, m_isGroup( false )
, m_isCritical( parent ? parent->isCritical() : false )
, m_showReadOnly( parent ? parent->isImmutable() : false )
, m_showNoncheckable( false )
{
}
PackageTreeItem::PackageTreeItem( const QVariantMap& groupData, PackageTag&& parent )
: m_parentItem( parent.parent )
, m_packageName( Calamares::getString( groupData, "name" ) )
, m_selected( parentCheckState( parent.parent ) )
, m_description( Calamares::getString( groupData, "description" ) )
, m_isGroup( false )
, m_isCritical( parent.parent ? parent.parent->isCritical() : false )
, m_showReadOnly( parent.parent ? parent.parent->isImmutable() : false )
, m_showNoncheckable( false )
{
}
PackageTreeItem::PackageTreeItem( const QVariantMap& groupData, GroupTag&& parent )
: m_parentItem( parent.parent )
, m_name( Calamares::getString( groupData, "name" ) )
, m_selected( parentCheckState( parent.parent ) )
, m_description( Calamares::getString( groupData, "description" ) )
, m_preScript( Calamares::getString( groupData, "pre-install" ) )
, m_postScript( Calamares::getString( groupData, "post-install" ) )
, m_source( Calamares::getString( groupData, "source" ) )
, m_isGroup( true )
, m_isCritical( parentCriticality( groupData, parent.parent ) )
, m_isHidden( Calamares::getBool( groupData, "hidden", false ) )
, m_showReadOnly( Calamares::getBool( groupData, "immutable", false ) )
, m_showNoncheckable( Calamares::getBool( groupData, "noncheckable", false ) )
, m_startExpanded( Calamares::getBool( groupData, "expanded", false ) )
{
}
PackageTreeItem::PackageTreeItem::PackageTreeItem()
: m_parentItem( nullptr )
, m_name( QStringLiteral( "<root>" ) )
, m_selected( Qt::Checked )
, m_isGroup( true )
{
}
PackageTreeItem::~PackageTreeItem()
{
qDeleteAll( m_childItems );
}
void
PackageTreeItem::appendChild( PackageTreeItem* child )
{
m_childItems.append( child );
}
PackageTreeItem*
PackageTreeItem::child( int row )
{
return m_childItems.value( row );
}
int
PackageTreeItem::childCount() const
{
return m_childItems.count();
}
int
PackageTreeItem::row() const
{
if ( m_parentItem )
{
return m_parentItem->m_childItems.indexOf( const_cast< PackageTreeItem* >( this ) );
}
return 0;
}
QVariant
PackageTreeItem::data( int column ) const
{
switch ( column )
{
case 0:
// packages have a packagename, groups don't
return QVariant( isPackage() ? packageName() : name() );
case 1:
// packages often have a blank description
return QVariant( description() );
default:
return QVariant();
}
}
PackageTreeItem*
PackageTreeItem::parentItem()
{
return m_parentItem;
}
const PackageTreeItem*
PackageTreeItem::parentItem() const
{
return m_parentItem;
}
bool
PackageTreeItem::hiddenSelected() const
{
if ( !m_isHidden )
{
return m_selected != Qt::Unchecked;
}
if ( m_selected == Qt::Unchecked )
{
return false;
}
const PackageTreeItem* currentItem = parentItem();
while ( currentItem != nullptr )
{
if ( !currentItem->isHidden() )
{
return currentItem->isSelected() != Qt::Unchecked;
}
currentItem = currentItem->parentItem();
}
/* Has no non-hidden parents */
return m_selected != Qt::Unchecked;
}
void
PackageTreeItem::setSelected( Qt::CheckState isSelected )
{
if ( parentItem() == nullptr )
{
// This is the root, it is always checked so don't change state
return;
}
m_selected = isSelected;
setChildrenSelected( isSelected );
// Look for suitable parent item which may change checked-state
// when one of its children changes.
PackageTreeItem* currentItem = parentItem();
while ( ( currentItem != nullptr ) && ( currentItem->childCount() == 0 ) )
{
currentItem = currentItem->parentItem();
}
if ( currentItem == nullptr )
{
// Reached the root .. don't bother
return;
}
currentItem->updateSelected();
}
void
PackageTreeItem::updateSelected()
{
// Figure out checked-state based on the children
int childrenSelected = 0;
int childrenPartiallySelected = 0;
for ( int i = 0; i < childCount(); i++ )
{
if ( child( i )->isSelected() == Qt::Checked )
{
childrenSelected++;
}
if ( child( i )->isSelected() == Qt::PartiallyChecked )
{
childrenPartiallySelected++;
}
}
if ( !childrenSelected && !childrenPartiallySelected )
{
setSelected( Qt::Unchecked );
}
else if ( childrenSelected == childCount() )
{
setSelected( Qt::Checked );
}
else
{
setSelected( Qt::PartiallyChecked );
}
}
void
PackageTreeItem::setChildrenSelected( Qt::CheckState isSelected )
{
if ( isSelected != Qt::PartiallyChecked )
{
// Children are never root; don't need to use setSelected on them.
for ( auto child : m_childItems )
{
child->m_selected = isSelected;
child->setChildrenSelected( isSelected );
}
}
}
void
PackageTreeItem::removeChild( int row )
{
if ( 0 <= row && row < m_childItems.count() )
{
m_childItems.removeAt( row );
}
else
{
cWarning() << "Attempt to remove invalid child in removeChild() at row " << row;
}
}
int
PackageTreeItem::type() const
{
return QStandardItem::UserType;
}
QVariant
PackageTreeItem::toOperation() const
{
// If it's a package with a pre- or post-script, replace
// with the more complicated datastructure.
if ( !m_preScript.isEmpty() || !m_postScript.isEmpty() )
{
QMap< QString, QVariant > sdetails;
sdetails.insert( "pre-script", m_preScript );
sdetails.insert( "package", m_packageName );
sdetails.insert( "post-script", m_postScript );
return sdetails;
}
else
{
return m_packageName;
}
}
bool
PackageTreeItem::operator==( const PackageTreeItem& rhs ) const
{
if ( isGroup() != rhs.isGroup() )
{
// Different kinds
return false;
}
if ( isGroup() )
{
return name() == rhs.name() && description() == rhs.description() && preScript() == rhs.preScript()
&& postScript() == rhs.postScript() && isCritical() == rhs.isCritical() && isHidden() == rhs.isHidden()
&& m_showReadOnly == rhs.m_showReadOnly && expandOnStart() == rhs.expandOnStart();
}
else
{
return packageName() == rhs.packageName();
}
}

View File

@@ -0,0 +1,179 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2017 Kyle Robbertze <kyle@aims.ac.za>
* SPDX-FileCopyrightText: 2017 2020, Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef PACKAGETREEITEM_H
#define PACKAGETREEITEM_H
#include <QList>
#include <QStandardItem>
#include <QVariant>
class PackageTreeItem : public QStandardItem
{
public:
using List = QList< PackageTreeItem* >;
///@brief A tag class to distinguish package-from-map from group-from-map
struct PackageTag
{
PackageTreeItem* parent;
};
///@brief A tag class to distinguish group-from-map from package-from-map
struct GroupTag
{
PackageTreeItem* parent;
};
///@brief A package (individual package)
explicit PackageTreeItem( const QString& packageName, PackageTreeItem* parent = nullptr );
///@brief A package (individual package with description)
explicit PackageTreeItem( const QVariantMap& packageData, PackageTag&& parent );
///@brief A group (sub-items and sub-groups are ignored)
explicit PackageTreeItem( const QVariantMap& groupData, GroupTag&& parent );
///@brief A root item, always selected, named "<root>"
explicit PackageTreeItem();
~PackageTreeItem() override;
void appendChild( PackageTreeItem* child );
PackageTreeItem* child( int row );
int childCount() const;
QVariant data( int column ) const override;
int row() const;
PackageTreeItem* parentItem();
const PackageTreeItem* parentItem() const;
QString name() const { return m_name; }
QString packageName() const { return m_packageName; }
QString description() const { return m_description; }
QString preScript() const { return m_preScript; }
QString postScript() const { return m_postScript; }
QString source() const { return m_source; }
/** @brief Is this item a group-item?
*
* Groups have a (possibly empty) list of packages, and a
* (possibly empty) list of sub-groups, and can be marked
* critical, hidden, etc. Packages, on the other hand, only
* have a meaningful packageName() and selection status.
*
* Root is a group.
*/
bool isGroup() const { return m_isGroup; }
/// @brief Is this item a single package?
bool isPackage() const { return !isGroup(); }
/** @brief Is this item hidden?
*
* Hidden items (generally only groups) are maintained separately,
* not shown to the user, but do enter into the package-installation process.
*/
bool isHidden() const { return m_isHidden; }
/** @brief Is this hidden item, considered "selected"?
*
* This asserts when called on a non-hidden item.
* A hidden item has its own selected state, but really
* falls under the selectedness of the parent item.
*/
bool hiddenSelected() const;
/** @brief Is this group critical?
*
* A critical group must be successfully installed, for the Calamares
* installation to continue.
*/
bool isCritical() const { return m_isCritical; }
/** @brief Is this group expanded on start?
*
* This does not affect installation, only the UI. A group
* that expands on start is shown expanded (not collapsed)
* in the treeview when the page is loaded.
*/
bool expandOnStart() const { return m_startExpanded; }
/** @brief Is this an immutable item?
*
* Groups can be immutable: then you can't toggle the selected
* state of any of its items.
*/
bool isImmutable() const { return m_showReadOnly; }
/** @brief Is this a non-checkable item?
*
* Groups can be non-checkable: then you can't toggle the selected
* state of the group. This does not affect subgroups or packages.
*/
bool isNoncheckable() const { return m_showNoncheckable; }
/** @brief is this item selected?
*
* Groups may be partially selected; packages are only on or off.
*/
Qt::CheckState isSelected() const { return m_selected; }
/** @brief Turns this item into a variant for PackageOperations use
*
* For "plain" items, this is just the package name; items with
* scripts return a map. See the package module for how it's interpreted.
*/
QVariant toOperation() const;
void setSelected( Qt::CheckState isSelected );
void setChildrenSelected( Qt::CheckState isSelected );
void removeChild( int row );
/** @brief Update selectedness based on the children's states
*
* This only makes sense for groups, which might have packages
* or subgroups; it checks only direct children.
*/
void updateSelected();
// QStandardItem methods
int type() const override;
/** @brief Are two items equal
*
* This **disregards** parent-item and the child-items, and compares
* only the fields for the items-proper (name, .. expanded). Note
* also that *isSelected()* is a run-time state, and is **not**
* compared either.
*/
bool operator==( const PackageTreeItem& rhs ) const;
bool operator!=( const PackageTreeItem& rhs ) const { return !( *this == rhs ); }
private:
PackageTreeItem* m_parentItem;
List m_childItems;
// An entry can be a package, or a group.
QString m_name;
QString m_packageName;
Qt::CheckState m_selected = Qt::Unchecked;
// These are only useful for groups
QString m_description;
QString m_preScript;
QString m_postScript;
QString m_source;
bool m_isGroup = false;
bool m_isCritical = false;
bool m_isHidden = false;
bool m_showReadOnly = false;
bool m_showNoncheckable = false;
bool m_startExpanded = false;
};
#endif // PACKAGETREEITEM_H

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
SPDX-License-Identifier: GPL-3.0-or-later

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,7 +1,5 @@
<RCC>
<qresource prefix="/">
<file>images/no-selection.png</file>
<file>images/calamares.png</file>
<file>images/if.png</file>
</qresource>
</RCC>

View File

@@ -1,69 +0,0 @@
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
# SPDX-FileCopyrightText: 2021 Anke Boersma <demm@kaosx.us>
# SPDX-License-Identifier: BSD-2-Clause
#
if( NOT Calamares_WITH_QML )
calamares_skip_module( "packagechooserq (QML is not supported in this build)" )
return()
endif()
find_package(${qtname} ${QT_VERSION} CONFIG REQUIRED Core)
# Add optional libraries here
set(USER_EXTRA_LIB)
# include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../packagechooser )
set(_packagechooser ${CMAKE_CURRENT_SOURCE_DIR}/../packagechooser)
include_directories(${_packagechooser})
### OPTIONAL AppData XML support in PackageModel
#
#
option(BUILD_APPDATA "Support appdata: items in PackageChooser (requires QtXml)" ON)
if(BUILD_APPDATA)
find_package(${qtname} COMPONENTS Xml)
if(TARGET ${qtname}::Xml)
add_definitions(-DHAVE_APPDATA)
list(APPEND _extra_libraries ${qtname}::Xml)
list(APPEND _extra_src ${_packagechooser}/ItemAppData.cpp)
endif()
endif()
### OPTIONAL AppStream support in PackageModel
#
#
option(BUILD_APPSTREAM "Support appstream: items in PackageChooser (requires libappstream-qt)" ON)
if(BUILD_APPSTREAM)
find_package(AppStreamQt)
set_package_properties(
AppStreamQt
PROPERTIES
DESCRIPTION "Support for AppStream (cache) data"
URL "https://github.com/ximion/appstream"
PURPOSE "AppStream provides package data"
TYPE OPTIONAL
)
if(AppStreamQt_FOUND)
add_definitions(-DHAVE_APPSTREAM_VERSION=${AppStreamQt_VERSION_MAJOR})
list(APPEND _extra_libraries AppStreamQt)
list(APPEND _extra_src ${_packagechooser}/ItemAppStream.cpp)
endif()
endif()
calamares_add_plugin(packagechooserq
TYPE viewmodule
EXPORT_MACRO PLUGINDLLEXPORT_PRO
SOURCES
PackageChooserQmlViewStep.cpp
${_packagechooser}/Config.cpp
${_packagechooser}/PackageModel.cpp
${_extra_src}
RESOURCES
packagechooserq${QT_VERSION_SUFFIX}.qrc
LINK_PRIVATE_LIBRARIES
calamaresui
${_extra_libraries}
SHARED_LIB
)

View File

@@ -1,86 +0,0 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-FileCopyrightText: 2021 Anke Boersma <demm@kaosx.us>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "PackageChooserQmlViewStep.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "locale/TranslatableConfiguration.h"
#include "utils/Logger.h"
#include "utils/System.h"
#include "utils/Variant.h"
CALAMARES_PLUGIN_FACTORY_DEFINITION( PackageChooserQmlViewStepFactory, registerPlugin< PackageChooserQmlViewStep >(); )
PackageChooserQmlViewStep::PackageChooserQmlViewStep( QObject* parent )
: Calamares::QmlViewStep( parent )
, m_config( new Config( this ) )
{
emit nextStatusChanged( true );
}
QString
PackageChooserQmlViewStep::prettyName() const
{
return m_config->prettyName();
}
QString
PackageChooserQmlViewStep::prettyStatus() const
{
//QString option = m_pkgc;
//return tr( "Install option: %1" ).arg( option );
return m_config->prettyStatus();
}
bool
PackageChooserQmlViewStep::isNextEnabled() const
{
return true;
}
bool
PackageChooserQmlViewStep::isBackEnabled() const
{
return true;
}
bool
PackageChooserQmlViewStep::isAtBeginning() const
{
return true;
}
bool
PackageChooserQmlViewStep::isAtEnd() const
{
return true;
}
Calamares::JobList
PackageChooserQmlViewStep::jobs() const
{
Calamares::JobList l;
return l;
}
void
PackageChooserQmlViewStep::onLeave()
{
m_config->updateGlobalStorage();
}
void
PackageChooserQmlViewStep::setConfigurationMap( const QVariantMap& configurationMap )
{
m_config->setDefaultId( moduleInstanceKey() );
m_config->setConfigurationMap( configurationMap );
Calamares::QmlViewStep::setConfigurationMap( configurationMap ); // call parent implementation last
}

View File

@@ -1,58 +0,0 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* SPDX-FileCopyrightText: 2021 Anke Boersma <demm@kaosx.us>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef PACKAGECHOOSERQMLVIEWSTEP_H
#define PACKAGECHOOSERQMLVIEWSTEP_H
// Config from packagechooser module
#include "Config.h"
#include "DllMacro.h"
#include "locale/TranslatableConfiguration.h"
#include "utils/PluginFactory.h"
#include "viewpages/QmlViewStep.h"
#include <QVariantMap>
class Config;
class PackageChooserPage;
class PLUGINDLLEXPORT PackageChooserQmlViewStep : public Calamares::QmlViewStep
{
Q_OBJECT
public:
explicit PackageChooserQmlViewStep( QObject* parent = nullptr );
QString prettyName() const override;
QString prettyStatus() const override;
bool isNextEnabled() const override;
bool isBackEnabled() const override;
bool isAtBeginning() const override;
bool isAtEnd() const override;
//void onActivate() override;
void onLeave() override;
Calamares::JobList jobs() const override;
void setConfigurationMap( const QVariantMap& configurationMap ) override;
QObject* getConfig() override { return m_config; }
private:
Config* m_config;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( PackageChooserQmlViewStepFactory )
#endif // PACKAGECHOOSERQMLVIEWSTEP_H

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2020 demmm <anke62@gmail.com>
SPDX-License-Identifier: GPL-3.0-or-later

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

View File

@@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2020 demmm <anke62@gmail.com>
SPDX-License-Identifier: GPL-3.0-or-later

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2021 pngegg <https://www.pngegg.com/>
SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -1,304 +0,0 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2021 Anke Boersma <demm@kaosx.us>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
width: parent.width
height: parent.height
Rectangle {
anchors.fill: parent
color: "#f2f2f2"
ButtonGroup {
id: switchGroup
}
Column {
id: column
anchors.centerIn: parent
spacing: 5
Rectangle {
//id: rectangle
width: 700
height: 150
color: "#ffffff"
radius: 10
border.width: 0
Text {
width: 450
height: 104
anchors.centerIn: parent
text: qsTr("OpenRC base system.<br/>
Default option.")
font.pointSize: 10
anchors.verticalCenterOffset: -10
anchors.horizontalCenterOffset: 100
wrapMode: Text.WordWrap
}
Switch {
id: element1
x: 500
y: 110
width: 187
height: 14
text: qsTr("OpenRC")
checked: true
hoverEnabled: true
ButtonGroup.group: switchGroup
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 14
radius: 10
color: element1.checked ? "#3498db" : "#B9B9B9"
border.color: element1.checked ? "#3498db" : "#cccccc"
Rectangle {
x: element1.checked ? parent.width - width : 0
y: (parent.height - height) / 2
width: 20
height: 20
radius: 10
color: element1.down ? "#cccccc" : "#ffffff"
border.color: element1.checked ? (element1.down ? "#3498db" : "#3498db") : "#999999"
}
}
onCheckedChanged: {
if ( checked ) {
config.packageChoice = "openrc"
}
}
}
Image {
id: image1
x: 8
y: 25
height: 100
fillMode: Image.PreserveAspectFit
source: "images/if.png"
}
}
Rectangle {
width: 700
height: 150
radius: 10
border.width: 0
Text {
width: 450
height: 104
anchors.centerIn: parent
text: qsTr("Dinit base system.")
font.pointSize: 10
anchors.verticalCenterOffset: -10
anchors.horizontalCenterOffset: 100
wrapMode: Text.WordWrap
}
Switch {
id: element2
x: 500
y: 110
width: 187
height: 14
text: qsTr("Dinit")
checked: false
hoverEnabled: true
ButtonGroup.group: switchGroup
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 14
radius: 10
color: element2.checked ? "#3498db" : "#B9B9B9"
border.color: element2.checked ? "#3498db" : "#cccccc"
Rectangle {
x: element2.checked ? parent.width - width : 0
y: (parent.height - height) / 2
width: 20
height: 20
radius: 10
color: element2.down ? "#cccccc" : "#ffffff"
border.color: element2.checked ? (element2.down ? "#3498db" : "#3498db") : "#999999"
}
}
onCheckedChanged: {
if ( checked ) {
config.packageChoice = "dinit"
}
}
}
Image {
id: image2
x: 8
y: 25
height: 100
fillMode: Image.PreserveAspectFit
source: "images/if.png"
}
}
Rectangle {
width: 700
height: 150
color: "#ffffff"
radius: 10
border.width: 0
Text {
width: 450
height: 104
anchors.centerIn: parent
text: qsTr("Runit base system.")
font.pointSize: 10
anchors.verticalCenterOffset: -10
anchors.horizontalCenterOffset: 100
wrapMode: Text.WordWrap
}
Switch {
id: element3
x: 500
y: 110
width: 187
height: 14
text: qsTr("Runit")
checked: false
hoverEnabled: true
ButtonGroup.group: switchGroup
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 14
radius: 10
color: element3.checked ? "#3498db" : "#B9B9B9"
border.color: element3.checked ? "#3498db" : "#cccccc"
Rectangle {
x: element3.checked ? parent.width - width : 0
y: (parent.height - height) / 2
width: 20
height: 20
radius: 10
color: element3.down ? "#cccccc" : "#ffffff"
border.color: element3.checked ? (element3.down ? "#3498db" : "#3498db") : "#999999"
}
}
onCheckedChanged: {
if ( checked ) {
config.packageChoice = "runit"
}
}
}
Image {
id: image3
x: 8
y: 25
height: 100
fillMode: Image.PreserveAspectFit
source: "images/if.png"
}
}
Rectangle {
width: 700
height: 150
color: "#ffffff"
radius: 10
border.width: 0
Text {
width: 450
height: 104
anchors.centerIn: parent
text: qsTr("S6 base system.")
font.pointSize: 10
anchors.verticalCenterOffset: -10
anchors.horizontalCenterOffset: 100
wrapMode: Text.WordWrap
}
Switch {
id: element4
x: 500
y: 110
width: 187
height: 14
text: qsTr("S6")
checked: false
hoverEnabled: true
ButtonGroup.group: switchGroup
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 14
radius: 10
color: element4.checked ? "#3498db" : "#B9B9B9"
border.color: element4.checked ? "#3498db" : "#cccccc"
Rectangle {
x: element4.checked ? parent.width - width : 0
y: (parent.height - height) / 2
width: 20
height: 20
radius: 10
color: element4.down ? "#cccccc" : "#ffffff"
border.color: element4.checked ? (element4.down ? "#3498db" : "#3498db") : "#999999"
}
}
onCheckedChanged: {
if ( checked ) {
config.packageChoice = "s6"
}
}
}
Image {
id: image4
x: 8
y: 25
height: 100
fillMode: Image.PreserveAspectFit
source: "images/if.png"
}
}
Rectangle {
width: 700
height: 25
color: "#f2f2f2"
border.width: 0
Text {
height: 25
anchors.centerIn: parent
text: qsTr("Please select an option for your install, or use the default: OpenRC.")
font.pointSize: 10
wrapMode: Text.WordWrap
}
}
}
}
}

View File

@@ -1,9 +0,0 @@
<RCC>
<qresource>
<file alias="packagechooserq.qml">packagechooserq-qt6.qml</file>
<file>images/libreoffice.jpg</file>
<file>images/no-selection.png</file>
<file>images/plasma.png</file>
<file>images/if.png</file>
</qresource>
</RCC>

View File

@@ -1,66 +0,0 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
#
# Configuration for the low-density software chooser, QML implementation
#
# The example QML implementation uses single-selection, rather than
# a model for the available packages. That makes it simpler: the
# QML itself codes the available options, descriptions and images
# -- after all, this is **low density** selection, so a custom UI
# can make sense for the few choices that need to be made.
#
#
---
# Software installation method:
#
# - "legacy" or "custom" or "contextualprocess"
# When set to "legacy", writes a GlobalStorage value for the choice that
# has been made. The key is *packagechooser_<id>*. The module's
# instance name is used; see the *instances* section of `settings.conf`.
# If there is just one packagechooserq module, and no special instance is set,
# resulting GS key is probably *packagechooser_packagechooserq*.
# (Do note that the prefix of the GS key remains "packagechooser_")
#
# The GS value is a comma-separated list of the IDs of the selected
# packages, or an empty string if none is selected.
#
# With "legacy" installation, you should have a contextualprocess or similar
# module somewhere in the `exec` phase to process the GlobalStorage key
# and actually **do** something for the packages.
#
# - "packages"
# When set to "packages", writes GlobalStorage values suitable for
# consumption by the *packages* module (which should appear later
# in the `exec` section. These package settings will then be handed
# off to whatever package manager is configured there.
#
# There is no need to put this module in the `exec` section. There
# are no jobs that this module provides. You should put **other**
# modules, either *contextualprocess* or *packages* or some custom
# module, in the `exec` section to do the actual work.
#
method: legacy
# Human-visible strings in this module. These are all optional.
# The following translated keys are used:
# - *step*, used in the overall progress view (left-hand pane)
#
# Each key can have a [locale] added to it, which is used as
# the translated string for that locale. For the strings
# associated with the "no-selection" item, see *items*, below
# with the explicit item-*id* "".
#
labels:
step: "Packages"
step[nl]: "Pakketten"
# The *packageChoice* value is used for setting the default selection
# in the QML view; this should match one of the keys used in the QML
# module for package names.
#
# (e.g. the sample QML uses "no_office_suite", "minimal_install" and
# "libreoffice" as possible choices).
#
packageChoice: libreoffice

View File

@@ -1,304 +0,0 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2021 Anke Boersma <demm@kaosx.us>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
Item {
width: parent.width
height: parent.height
Rectangle {
anchors.fill: parent
color: "#f2f2f2"
ButtonGroup {
id: switchGroup
}
Column {
id: column
anchors.centerIn: parent
spacing: 5
Rectangle {
//id: rectangle
width: 700
height: 150
color: "#ffffff"
radius: 10
border.width: 0
Text {
width: 450
height: 104
anchors.centerIn: parent
text: qsTr("OpenRC base system.<br/>
Default option.")
font.pointSize: 10
anchors.verticalCenterOffset: -10
anchors.horizontalCenterOffset: 100
wrapMode: Text.WordWrap
}
Switch {
id: element1
x: 500
y: 110
width: 187
height: 14
text: qsTr("OpenRC")
checked: true
hoverEnabled: true
ButtonGroup.group: switchGroup
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 14
radius: 10
color: element1.checked ? "#3498db" : "#B9B9B9"
border.color: element1.checked ? "#3498db" : "#cccccc"
Rectangle {
x: element1.checked ? parent.width - width : 0
y: (parent.height - height) / 2
width: 20
height: 20
radius: 10
color: element1.down ? "#cccccc" : "#ffffff"
border.color: element1.checked ? (element1.down ? "#3498db" : "#3498db") : "#999999"
}
}
onCheckedChanged: {
if ( checked ) {
config.packageChoice = "openrc"
}
}
}
Image {
id: image1
x: 8
y: 25
height: 100
fillMode: Image.PreserveAspectFit
source: "images/if.png"
}
}
Rectangle {
width: 700
height: 150
radius: 10
border.width: 0
Text {
width: 450
height: 104
anchors.centerIn: parent
text: qsTr("Dinit base system.")
font.pointSize: 10
anchors.verticalCenterOffset: -10
anchors.horizontalCenterOffset: 100
wrapMode: Text.WordWrap
}
Switch {
id: element2
x: 500
y: 110
width: 187
height: 14
text: qsTr("Dinit")
checked: false
hoverEnabled: true
ButtonGroup.group: switchGroup
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 14
radius: 10
color: element2.checked ? "#3498db" : "#B9B9B9"
border.color: element2.checked ? "#3498db" : "#cccccc"
Rectangle {
x: element2.checked ? parent.width - width : 0
y: (parent.height - height) / 2
width: 20
height: 20
radius: 10
color: element2.down ? "#cccccc" : "#ffffff"
border.color: element2.checked ? (element2.down ? "#3498db" : "#3498db") : "#999999"
}
}
onCheckedChanged: {
if ( checked ) {
config.packageChoice = "dinit"
}
}
}
Image {
id: image2
x: 8
y: 25
height: 100
fillMode: Image.PreserveAspectFit
source: "images/if.png"
}
}
Rectangle {
width: 700
height: 150
color: "#ffffff"
radius: 10
border.width: 0
Text {
width: 450
height: 104
anchors.centerIn: parent
text: qsTr("Runit base system.")
font.pointSize: 10
anchors.verticalCenterOffset: -10
anchors.horizontalCenterOffset: 100
wrapMode: Text.WordWrap
}
Switch {
id: element3
x: 500
y: 110
width: 187
height: 14
text: qsTr("Runit")
checked: false
hoverEnabled: true
ButtonGroup.group: switchGroup
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 14
radius: 10
color: element3.checked ? "#3498db" : "#B9B9B9"
border.color: element3.checked ? "#3498db" : "#cccccc"
Rectangle {
x: element3.checked ? parent.width - width : 0
y: (parent.height - height) / 2
width: 20
height: 20
radius: 10
color: element3.down ? "#cccccc" : "#ffffff"
border.color: element3.checked ? (element3.down ? "#3498db" : "#3498db") : "#999999"
}
}
onCheckedChanged: {
if ( checked ) {
config.packageChoice = "runit"
}
}
}
Image {
id: image3
x: 8
y: 25
height: 100
fillMode: Image.PreserveAspectFit
source: "images/if.png"
}
}
Rectangle {
width: 700
height: 150
color: "#ffffff"
radius: 10
border.width: 0
Text {
width: 450
height: 104
anchors.centerIn: parent
text: qsTr("S6 base system.")
font.pointSize: 10
anchors.verticalCenterOffset: -10
anchors.horizontalCenterOffset: 100
wrapMode: Text.WordWrap
}
Switch {
id: element4
x: 500
y: 110
width: 187
height: 14
text: qsTr("S6")
checked: false
hoverEnabled: true
ButtonGroup.group: switchGroup
indicator: Rectangle {
implicitWidth: 40
implicitHeight: 14
radius: 10
color: element4.checked ? "#3498db" : "#B9B9B9"
border.color: element4.checked ? "#3498db" : "#cccccc"
Rectangle {
x: element4.checked ? parent.width - width : 0
y: (parent.height - height) / 2
width: 20
height: 20
radius: 10
color: element4.down ? "#cccccc" : "#ffffff"
border.color: element4.checked ? (element4.down ? "#3498db" : "#3498db") : "#999999"
}
}
onCheckedChanged: {
if ( checked ) {
config.packageChoice = "s6"
}
}
}
Image {
id: image4
x: 8
y: 25
height: 100
fillMode: Image.PreserveAspectFit
source: "images/if.png"
}
}
Rectangle {
width: 700
height: 25
color: "#f2f2f2"
border.width: 0
Text {
height: 25
anchors.centerIn: parent
text: qsTr("Please select an option for your install, or use the default: OpenRC.")
font.pointSize: 10
wrapMode: Text.WordWrap
}
}
}
}
}

View File

@@ -1,9 +0,0 @@
<RCC>
<qresource>
<file>packagechooserq.qml</file>
<file>images/libreoffice.jpg</file>
<file>images/no-selection.png</file>
<file>images/plasma.png</file>
<file>images/if.png</file>
</qresource>
</RCC>

View File

@@ -1,80 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# === This file is part of Calamares - <http://github.com/calamares> ===
#
# Copyright 2014 - 2016, Philip Müller <philm@manjaro.org>
# Copyright 2016, Artoo <artoo@manjaro.org>
# Copyright 2018, Artoo <artoo@artixlinux.org>
#
# Calamares is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Calamares is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Calamares. If not, see <http://www.gnu.org/licenses/>.
from os.path import join, exists
import libcalamares
from libcalamares.utils import target_env_call
class ConfigController:
"""Configuration controller
"""
def __init__(self):
self.root = libcalamares.globalstorage.value("rootMountPoint")
def terminate(self, proc):
"""Send SIGKILL to the given proccess
"""
target_env_call(['killall', '-9', proc])
def sedFile(self, pattern, file):
"""Sed the given file with the given pattern
"""
target_env_call(["sed", "-e", pattern, "-i", file])
def configure(self):
"""Configure the services
"""
if exists(join(self.root, "/etc/conf.d/keymaps")):
exp = 's|^.*keymap=.*|keymap="{}"|'.format(
libcalamares.globalstorage.value("keyboardLayout")
)
self.sedFile(exp, "/etc/conf.d/keymaps")
if exists(join(self.root, "/etc/conf.d/xdm")):
for dm in libcalamares.globalstorage.value("displayManagers"):
exp = 's|^.*DISPLAYMANAGER=.*|DISPLAYMANAGER="{}"|'.format(dm)
self.sedFile(exp, "/etc/conf.d/xdm")
def run(self):
"""Run the controller
Workaround for pacman-key bug
FS#45351 https://bugs.archlinux.org/task/45351
We have to kill gpg-agent because if it stays
around we can't reliably unmount
the target partition.
"""
self.configure()
self.terminate('gpg-agent')
def run():
""" Misc postinstall configurations """
config = ConfigController()
return config.run()

View File

@@ -1,7 +0,0 @@
# Syntax is YAML 1.2
---
type: "job"
name: "postcfg"
interface: "python"
script: "main.py" #assumed relative to the current directory
noconfig: true

View File

@@ -1,104 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2016 Artoo <artoo@manjaro.org>
# SPDX-FileCopyrightText: 2017 Philip Müller <philm@manjaro.org>
# SPDX-FileCopyrightText: 2018 Artoo <artoo@artixlinux.org>
# SPDX-FileCopyrightText: 2018-2019 Adriaan de Groot <groot@kde.org>
# SPDX-FileCopyrightText: 2023-2024 Artoo <artoo@artixlinux.org>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Calamares is Free Software: see the License-Identifier above.
#
import libcalamares
import gettext
_ = gettext.translation("calamares-python",
localedir=libcalamares.utils.gettext_path(),
languages=libcalamares.utils.gettext_languages(),
fallback=True).gettext
def pretty_name():
return _("Configure services")
def manage_services(services, manager, cmd):
"""
For each entry in @p services, run "<command> <action> <name>",
where each service is a mapping of service name, action, and a flag.
Returns a failure message, or None if this was successful.
Services that are not mandatory have their failures suppressed
silently.
"""
for sv in services:
if isinstance(sv, str):
name = sv
action = "enable"
mandatory = False
alias = "add"
target = "default"
else:
if "name" not in sv:
libcalamares.utils.error("The key 'name' is missing from the mapping {_sv!s}. Continuing to the next service.".format(_sv=str(sv)))
continue
name = sv["name"]
action = sv.get("action", "enable")
mandatory = sv.get("mandatory", False)
alias = sv.get("alias", "add")
target = sv.get("target", "default")
command = [ cmd ]
match manager:
case "openrc":
args = [alias, name, target]
case "s6":
args = [alias, target, name]
case _:
args = [action, name]
command.extend(args)
libcalamares.utils.debug("Manager command: {!s}".format(command))
ec = libcalamares.utils.target_env_call(command)
if ec != 0:
libcalamares.utils.warning(
"Cannot {} service {}".format(action, name)
)
libcalamares.utils.warning(
"service {} call in chroot returned error code {}".format(action, ec)
)
if mandatory:
title = _("Cannot modify service")
diagnostic = _("<code>service {_action!s}</code> call in chroot returned error code {_ec!s}.").format(_action=action, _ec=ec)
description = _("Cannot {_action!s} service <code>{_name!s}</code>.").format(_action=action, _name=name)
return (
title,
description + " " + diagnostic
)
def run():
"""
Setup services
"""
manager = libcalamares.job.configuration.get("manager")
if libcalamares.globalstorage.contains("initProvider"):
manager = libcalamares.globalstorage.value("initProvider")
commands = libcalamares.job.configuration.get("commands")
cmd = commands.get(manager)
services = libcalamares.job.configuration.get("services", [])
r = manage_services(services, manager, cmd)
if r is not None:
return r

View File

@@ -1,7 +0,0 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
---
type: "job"
name: "services-artix"
interface: "python"
script: "main.py"

View File

@@ -1,74 +0,0 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
#
# openrc services module to modify service runlevels via rc-update in the chroot
#
# Services can be added (to any runlevel, or multiple runlevels) or deleted.
# Handle del with care and only use it if absolutely necessary.
#
# if a service is listed in the conf but is not present/detected on the target system,
# or a runlevel does not exist, it will be ignored and skipped; a warning is logged.
#
---
#
# Which service manager to use, options are:
# - openrc
# - dinit
# - s6
# - runit
#
manager: openrc
commands:
openrc: rc-update
dinit: artix-svc
s6: s6-service
runit: rsm
# There is one key for this module: *services*. Its value is a list of entries.
# Each entry has three keys:
# - *name* is the (string) name of the manager service that is being changed.
# Use quotes. You can use any valid initProvider service here (for example,
# "NetworkManager", "cupsd", "lightdm", "gdm", etc.)
# - *action* is the (string) action that you want to perform over the service
# (for example, "enable", "disable"). Please
# ensure that the action can actually run under chroot (otherwise it is
# pointless)
# - *mandatory* is a boolean option, which states whether the change
# must be done successfully. If manager reports an error while changing
# a mandatory entry, the installation will fail. When mandatory is false,
# errors for that manager service are ignored. If mandatory
# is not specified, the default is false.
#
# The order of operations is the same as the order in which entries
# appear in the list
# # This example enables NetworkManager.service (and fails if it can't),
# # disables cups.socket (and ignores failure). Then it enables the
# # graphical target (e.g. so that SDDM runs for login), and
# # finally masks pacman-init (an ArchLinux-only service).
# #
# services:
# - name: "NetworkManager"
# action: "enable"
# alias: "add"
# target: "default"
# mandatory: true
#
# - name: "cupsd"
# action: "disable"
# alias: "del"
# target: "default"
# # The property "mandatory" is taken to be false by default here
# # because it is not specified
#
# - name: "sddm"
# action: "enable"
# alias: "add"
# target: "default"
# # The property "mandatory" is taken to be false by default here
# # because it is not specified
services: []

View File

@@ -1,133 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# === This file is part of Calamares - <https://github.com/calamares> ===
#
# Copyright 2018-2019, Adriaan de Groot <groot@kde.org>
# Copyright 2021, Artoo <artoo@artixlinux.org>
#
# Calamares is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Calamares is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Calamares. If not, see <http://www.gnu.org/licenses/>.
import libcalamares
from libcalamares.utils import target_env_call, warning
from os.path import exists, join
import gettext
_ = gettext.translation("calamares-python",
localedir=libcalamares.utils.gettext_path(),
languages=libcalamares.utils.gettext_languages(),
fallback=True).gettext
def pretty_name():
return _("Configure Dinit services")
class DinitController:
"""
This is the dinit service controller.
All of its state comes from global storage and the job
configuration at initialization time.
"""
def __init__(self):
self.root = libcalamares.globalstorage.value('rootMountPoint')
# Translate the entries in the config to the actions passed to sv-helper
self.services = dict()
self.services["enable"] = libcalamares.job.configuration.get('services', [])
self.services["disable"] = libcalamares.job.configuration.get('disable', [])
self.initdDir = libcalamares.job.configuration['initdDir']
self.runsvDir = libcalamares.job.configuration['runsvDir']
def make_failure_description(self, state, name):
"""
Returns a generic "could not <foo>" failure message, specialized
for the action @p state and the specific service @p name.
"""
if state == "enable":
description = _("Cannot enable service {name!s}.")
elif state == "disable":
description = _("Cannot disable service {name!s}.")
else:
description = _("Unknown service-action <code>{arg!s}</code> for service {name!s}.")
return description.format(arg=state, name=name)
def update(self, state):
"""
Process each service listed
in services for the given @p state.
"""
for svc in self.services.get(state, []):
if isinstance(svc, str):
name = svc
mandatory = False
else:
name = svc["name"]
mandatory = svc.get("mandatory", False)
service_path = self.root + self.initdDir + "/" + name
runlevel_path = self.root + self.runsvDir
src = self.initdDir + "/" + name
dest = self.runsvDir + "/"
if state == 'enable':
cmd = ["ln", "-sv", src, dest]
elif state == 'disable':
cmd = ["rm", "-rv", dest]
if exists(service_path):
if exists(runlevel_path):
ec = target_env_call(cmd)
if ec != 0:
warning("Cannot {} service {}".format(state, name))
warning("{} returned error code {!s}".format(cmd, ec))
if mandatory:
title = _("Cannot modify service")
diagnostic = _("<code>cmd {arg!s}</code> call in chroot returned error code {num!s}.").format(arg=state, num=ec)
return (title,
self.make_failure_description(state, name) + " " + diagnostic
)
else:
warning("Target service {} does not exist in {}.".format(name, self.initdDir))
if mandatory:
title = _("Target service does not exist")
diagnostic = _("The path for service {name!s} is <code>{path!s}</code>, which does not exist.").format(name=name, path=service_path)
return (title,
self.make_failure_description(state, name) + " " + diagnostic
)
def run(self):
"""Run the controller
"""
for state in ("enable", "disable"):
r = self.update(state)
if r is not None:
return r
def run():
"""
Setup services
"""
return DinitController().run()

View File

@@ -1,5 +0,0 @@
---
type: "job"
name: "services-dinit"
interface: "python"
script: "main.py"

View File

@@ -1,41 +0,0 @@
# runit services module to modify service runlevels via symlinks in the chroot
#
# Services can be added (to any runlevel, or multiple runlevels) or deleted.
# Handle disable with care and only use it if absolutely necessary.
#
# if a service is listed in the conf but is not present/detected on the target system,
# it will be ignored and skipped; a warning is logged.
#
---
# initdDir: holds the runit service directory location
initdDir: /etc/dinit.d
# runsvDir: holds the runlevels directory location
runsvDir: /etc/dinit.d/boot.d
# services: a list of entries to **enable**
# disable: a list of entries to **disable**
#
# Each entry has three fields:
# - name: the service name
# - (optional) mandatory: if set to true, a failure to modify
# the service will result in installation failure, rather than just
# a warning. The default is false.
#
# an entry may also be a single string, which is interpreted
# as the name field.
#
# # Example services and disable settings:
# # - add foo1, but it must succeed
# # - add foo2
# # - remove foo3
# # - remove foo4
# services:
# - name: foo1
# mandatory: true
# - name: foo2
# disable:
# - name: foo3
# - foo4
services: []
disable: []

View File

@@ -1,144 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# === This file is part of Calamares - <https://github.com/calamares> ===
#
# Copyright 2018-2019, Adriaan de Groot <groot@kde.org>
# Copyright 2019, Artoo <artoo@artixlinux.org>
#
# Calamares is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Calamares is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Calamares. If not, see <http://www.gnu.org/licenses/>.
import libcalamares
from libcalamares.utils import target_env_call, warning
from os.path import exists, join
import gettext
_ = gettext.translation("calamares-python",
localedir=libcalamares.utils.gettext_path(),
languages=libcalamares.utils.gettext_languages(),
fallback=True).gettext
def pretty_name():
return _("Configure Runit services")
class RunitController:
"""
This is the runit service controller.
All of its state comes from global storage and the job
configuration at initialization time.
"""
def __init__(self):
self.root = libcalamares.globalstorage.value('rootMountPoint')
# Translate the entries in the config to the actions passed to sv-helper
self.services = dict()
self.services["enable"] = libcalamares.job.configuration.get('services', [])
self.services["disable"] = libcalamares.job.configuration.get('disable', [])
self.svDir = libcalamares.job.configuration['svDir']
self.runsvDir = libcalamares.job.configuration['runsvDir']
def make_failure_description(self, state, name, runlevel):
"""
Returns a generic "could not <foo>" failure message, specialized
for the action @p state and the specific service @p name in @p runlevel.
"""
if state == "enable":
description = _("Cannot enable service {name!s} to run-level {level!s}.")
elif state == "disable":
description = _("Cannot disable service {name!s} from run-level {level!s}.")
else:
description = _("Unknown service-action <code>{arg!s}</code> for service {name!s} in run-level {level!s}.")
return description.format(arg=state, name=name, level=runlevel)
def update(self, state):
"""
Call sv-helper for each service listed
in services for the given @p state.
"""
for svc in self.services.get(state, []):
if isinstance(svc, str):
name = svc
runlevel = "default"
mandatory = False
else:
name = svc["name"]
runlevel = svc.get("runlevel", "default")
mandatory = svc.get("mandatory", False)
service_path = self.root + self.svDir + "/" + name
runlevel_path = self.root + self.runsvDir + "/" + runlevel
src = self.svDir + "/" + name
dest = self.runsvDir + "/" + runlevel + "/"
if state == 'enable':
cmd = ["ln", "-sv", src, dest]
elif state == 'disable':
cmd = ["rm", "-rv", dest]
if exists(service_path):
if exists(runlevel_path):
ec = target_env_call(cmd)
if ec != 0:
warning("Cannot {} service {} to {}".format(state, name, runlevel))
warning("{} returned error code {!s}".format(cmd, ec))
if mandatory:
title = _("Cannot modify service")
diagnostic = _("<code>cmd {arg!s}</code> call in chroot returned error code {num!s}.").format(arg=state, num=ec)
return (title,
self.make_failure_description(state, name, runlevel) + " " + diagnostic
)
else:
warning("Target runlevel {} does not exist for {}.".format(runlevel, name))
if mandatory:
title = _("Target runlevel does not exist")
diagnostic = _("The path for runlevel {level!s} is <code>{path!s}</code>, which does not exist.").format(level=runlevel, path=runlevel_path)
return (title,
self.make_failure_description(state, name, runlevel) + " " + diagnostic
)
else:
warning("Target service {} does not exist in {}.".format(name, self.svDir))
if mandatory:
title = _("Target service does not exist")
diagnostic = _("The path for service {name!s} is <code>{path!s}</code>, which does not exist.").format(name=name, path=service_path)
return (title,
self.make_failure_description(state, name, runlevel) + " " + diagnostic
)
def run(self):
"""Run the controller
"""
for state in ("enable", "disable"):
r = self.update(state)
if r is not None:
return r
def run():
"""
Setup services
"""
return RunitController().run()

View File

@@ -1,5 +0,0 @@
---
type: "job"
name: "services-runit"
interface: "python"
script: "main.py"

Some files were not shown because too many files have changed in this diff Show More