:Merge branch 'calamares' of https://github.com/calamares/calamares into development

This commit is contained in:
Philip Müller 2020-07-30 12:48:59 +02:00
commit 5aee4d195d
86 changed files with 11110 additions and 2001 deletions

27
CHANGES
View File

@ -6,14 +6,33 @@ website will have to do for older versions.
# 3.2.28 (unreleased) #
This release contains contributions from (alphabetically by first name):
- No external contributors yet
- Anke Boersma
- apt-ghetto
## Core ##
- No core changes yet
- A new object *Network* is available to QML modules in `io.calamares.core`.
It exposes network status through the *hasInternet* property.
- Welcome to Tajik translations. This has reached sufficient completion
to be included in the drop-down list of languages on the welcome page.
- Welcome to [Interlingue](https://en.wikipedia.org/wiki/Interlingue).
The translation is at an early stage.
## Modules ##
- No module changes yet
- The *locale* module has been completely redone on the inside.
Users should see no changes. #1391
- The *localeq* module uses the redone internals of the locale module.
It can now be used to set timezone, language and locale information
and is a suitable alternative module. Thanks to Anke Boersma who did
the work of figuring out maps. Note that the map uses several GeoIP
and GeoData providers and you may need to configure the URLs
with suitable usernames for those services. #1426
- Both *locale* and *localeq* can now be configured to use the system's
timezone setting -- this can be useful to avoid both hard-coding an
initial zone and doing extra GeoIP lookups, in the case where the
live system already does so. #1391
- The *users* module no longer accepts `root` as a username. #1462
- The *keyboardq* module is now more inline with the look of the rest
of the Calamares modules, use of a background image is removed.
# 3.2.27 (2020-07-11) #

View File

@ -147,14 +147,15 @@ set( CALAMARES_DESCRIPTION_SUMMARY
# copy these four lines to four backup lines, add "p", and then update
# the original four lines with the current translations).
#
# Total 66 languages
set( _tx_complete az az_AZ ca he hi hr ja sq tr_TR uk zh_TW )
set( _tx_good as ast be cs_CZ da de es fi_FI fr hu it_IT ko lt ml
nl pt_BR pt_PT ru sk sv zh_CN )
# Total 68 languages
set( _tx_complete az az_AZ ca da fi_FI he hi hr ja pt_BR sq tr_TR
uk zh_TW )
set( _tx_good as ast be cs_CZ de es fr hu it_IT ko lt ml nl pt_PT
ru sk sv zh_CN )
set( _tx_ok ar bg el en_GB es_MX es_PR et eu fa gl id is mr nb pl
ro sl sr sr@latin th )
set( _tx_incomplete bn ca@valencia eo fr_CH gu kk kn lo lv mk ne_NP
ur uz )
ro sl sr sr@latin tg th )
set( _tx_incomplete bn ca@valencia eo fr_CH gu ie kk kn lo lv mk
ne_NP ur uz )
### Required versions
#
@ -355,23 +356,33 @@ set_package_properties(
URL "https://python.org"
PURPOSE "Python 3 interpreter for certain tests."
)
set( _schema_explanation "" )
if ( PYTHONINTERP_FOUND )
message(STATUS "Found Python 3 interpreter ${PYTHON_EXECUTABLE}")
if ( BUILD_SCHEMA_TESTING )
# The configuration validator script has some dependencies,
# and if they are not installed, don't run. If errors out
# with exit(1) on missing dependencies.
exec_program( ${PYTHON_EXECUTABLE} ARGS "${CMAKE_SOURCE_DIR}/ci/configvalidator.py" -x RETURN_VALUE _validator_deps )
if ( CALAMARES_CONFIGVALIDATOR_CHECKED )
set( _validator_deps ${CALAMARES_CONFIGVALIDATOR_RESULT} )
else()
exec_program( ${PYTHON_EXECUTABLE} ARGS "${CMAKE_SOURCE_DIR}/ci/configvalidator.py" -x RETURN_VALUE _validator_deps )
set( CALAMARES_CONFIGVALIDATOR_CHECKED TRUE CACHE INTERNAL "Dependencies for configvalidator checked" )
set( CALAMARES_CONFIGVALIDATOR_RESULT ${_validator_deps} CACHE INTERNAL "Result of configvalidator dependency check" )
endif()
# It should never succeed, but only returns 1 when the imports fail
if ( _validator_deps EQUAL 1 )
message(STATUS "BUILD_SCHEMA_TESTING dependencies are missing." )
set( _schema_explanation " Missing dependencies for configvalidator.py." )
set( BUILD_SCHEMA_TESTING OFF )
endif()
endif()
else()
# Can't run schema tests without Python3.
set( _schema_explanation " Missing Python3." )
set( BUILD_SCHEMA_TESTING OFF )
endif()
add_feature_info( yaml-schema BUILD_SCHEMA_TESTING "Validate YAML (config files) with schema.${_schema_explanation}" )
find_package( PythonLibs ${PYTHONLIBS_VERSION} )
set_package_properties(
PythonLibs PROPERTIES
@ -536,7 +547,11 @@ if( CALAMARES_VERSION_RC )
set( CALAMARES_VERSION ${CALAMARES_VERSION}rc${CALAMARES_VERSION_RC} )
endif()
# additional info for non-release builds
# Additional info for non-release builds. The "extended" version information
# with date and git information (commit, dirty status) is used only
# by CalamaresVersionX.h, which is included by consumers that need a full
# version number with all that information; normal consumers can include
# CalamaresVersion.h with more stable numbers.
if( NOT BUILD_RELEASE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git/" )
include( CMakeDateStamp )
set( CALAMARES_VERSION_DATE "${CMAKE_DATESTAMP_YEAR}${CMAKE_DATESTAMP_MONTH}${CMAKE_DATESTAMP_DAY}" )

View File

@ -62,10 +62,8 @@ function(calamares_add_library)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
# add resources from current dir
if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/${LIBRARY_RESOURCES}")
qt5_add_resources(LIBRARY_RC_SOURCES "${LIBRARY_RESOURCES}")
list(APPEND LIBRARY_SOURCES ${LIBRARY_RC_SOURCES})
unset(LIBRARY_RC_SOURCES)
if(LIBRARY_RESOURCES)
list(APPEND LIBRARY_SOURCES ${LIBRARY_RESOURCES})
endif()
# add target
@ -81,6 +79,9 @@ function(calamares_add_library)
if(LIBRARY_UI)
calamares_autouic(${target} ${LIBRARY_UI})
endif()
if(LIBRARY_RESOURCES)
calamares_autorcc(${target} ${LIBRARY_RESOURCES})
endif()
if(LIBRARY_EXPORT_MACRO)
set_target_properties(${target} PROPERTIES COMPILE_DEFINITIONS ${LIBRARY_EXPORT_MACRO})

View File

@ -22,50 +22,15 @@
include( CMakeParseArguments )
if( NOT _rcc_version_support_checked )
set( _rcc_version_support_checked TRUE )
# Extract the executable name
get_property( _rcc_executable
TARGET ${Qt5Core_RCC_EXECUTABLE}
PROPERTY IMPORTED_LOCATION
)
if( NOT _rcc_executable )
# Weird, probably now uses Qt5::rcc which is wrong too
set( _rcc_executable ${Qt5Core_RCC_EXECUTABLE} )
endif()
# Try an empty RCC file with explicit format-version
execute_process(
COMMAND echo "<RCC version='1.0'></RCC>"
COMMAND ${Qt5Core_RCC_EXECUTABLE} --format-version 1 --list -
RESULT_VARIABLE _rcc_version_rv
ERROR_VARIABLE _rcc_version_dump
)
if ( _rcc_version_rv EQUAL 0 )
# Supported: force to the reproducible version
set( _rcc_version_support --format-version 1 )
else()
# Older Qt versions (5.7, 5.8) don't support setting the
# rcc format-version, so won't be reproducible if they
# default to version 2.
set( _rcc_version_support "" )
endif()
unset( _rcc_version_rv )
unset( _rcc_version_dump )
endif()
# Internal macro for adding the C++ / Qt translations to the
# build and install tree. Should be called only once, from
# src/calamares/CMakeLists.txt.
macro(add_calamares_translations language)
list( APPEND CALAMARES_LANGUAGES ${ARGV} )
set( calamares_i18n_qrc_content "<!DOCTYPE RCC><RCC version=\"1.0\">\n" )
set( calamares_i18n_qrc_content "" )
# calamares and qt language files
set( calamares_i18n_qrc_content "${calamares_i18n_qrc_content}<qresource prefix=\"/lang\">\n" )
foreach( lang ${CALAMARES_LANGUAGES} )
foreach( tlsource "calamares_${lang}" "tz_${lang}" )
if( EXISTS "${CMAKE_SOURCE_DIR}/lang/${tlsource}.ts" )
@ -75,31 +40,19 @@ macro(add_calamares_translations language)
endforeach()
endforeach()
set( calamares_i18n_qrc_content "${calamares_i18n_qrc_content}</qresource>\n" )
set( calamares_i18n_qrc_content "${calamares_i18n_qrc_content}</RCC>\n" )
file( WRITE ${CMAKE_BINARY_DIR}/lang/calamares_i18n.qrc "${calamares_i18n_qrc_content}" )
qt5_add_translation(QM_FILES ${TS_FILES})
## HACK HACK HACK - around rcc limitations to allow out of source-tree building
set( trans_file calamares_i18n )
set( trans_srcfile ${CMAKE_BINARY_DIR}/lang/${trans_file}.qrc )
set( trans_infile ${CMAKE_CURRENT_BINARY_DIR}/${trans_file}.qrc )
set( trans_outfile ${CMAKE_CURRENT_BINARY_DIR}/qrc_${trans_file}.cxx )
# Copy the QRC file to the output directory
add_custom_command(
OUTPUT ${trans_infile}
COMMAND ${CMAKE_COMMAND} -E copy ${trans_srcfile} ${trans_infile}
MAIN_DEPENDENCY ${trans_srcfile}
)
configure_file( ${CMAKE_SOURCE_DIR}/lang/calamares_i18n.qrc.in ${trans_infile} @ONLY )
qt5_add_translation(QM_FILES ${TS_FILES})
# Run the resource compiler (rcc_options should already be set)
add_custom_command(
OUTPUT ${trans_outfile}
COMMAND "${Qt5Core_RCC_EXECUTABLE}"
ARGS ${rcc_options} ${_rcc_version_support} -name ${trans_file} -o ${trans_outfile} ${trans_infile}
ARGS ${rcc_options} --format-version 1 -name ${trans_file} -o ${trans_outfile} ${trans_infile}
MAIN_DEPENDENCY ${trans_infile}
DEPENDS ${QM_FILES}
)

View File

@ -18,17 +18,28 @@
#
###
#
# Helper function for doing automoc on a target, and autoui on a .ui file.
# Helper function for doing automoc, autouic, autorcc on targets,
# and on the corresponding .ui or .rcc files.
#
# Sets AUTOMOC TRUE for a target.
# calamares_automoc(target)
# Sets AUTOMOC TRUE for a target.
#
# If the global variable CALAMARES_AUTOMOC_OPTIONS is set, uses that
# as well to set options passed to MOC. This can be used to add
# libcalamares/utils/moc-warnings.h file to the moc, which in turn
# reduces compiler warnings in generated MOC code.
# If the global variable CALAMARES_AUTOMOC_OPTIONS is set, uses that
# as well to set options passed to MOC. This can be used to add
# libcalamares/utils/moc-warnings.h file to the moc, which in turn
# reduces compiler warnings in generated MOC code.
#
# If the global variable CALAMARES_AUTOUIC_OPTIONS is set, adds that
# to the options passed to uic.
# calamares_autouic(target [uifile ..])
# Sets AUTOUIC TRUE for a target.
#
# If the global variable CALAMARES_AUTOUIC_OPTIONS is set, adds that
# to the options passed to uic for each of the named uifiles.
#
# calamares_autorcc(target [rcfile ..])
# Sets AUTOUIC TRUE for a target.
#
# If the global variable CALAMARES_AUTORCC_OPTIONS is set, adds that
# to the options passed to rcc for each of the named rcfiles.
function(calamares_automoc TARGET)
set_target_properties( ${TARGET} PROPERTIES AUTOMOC TRUE )
@ -45,3 +56,12 @@ function(calamares_autouic TARGET)
endforeach()
endif()
endfunction()
function(calamares_autorcc TARGET)
set_target_properties( ${TARGET} PROPERTIES AUTORCC TRUE )
if ( CALAMARES_AUTORCC_OPTIONS )
foreach(S ${ARGN})
set_property(SOURCE ${S} PROPERTY AUTORCC_OPTIONS "${CALAMARES_AUTORCC_OPTIONS}")
endforeach()
endif()
endfunction()

View File

@ -94,6 +94,10 @@ Name[hr]=Instaliraj sustav
Icon[hr]=calamares
GenericName[hr]=Instalacija sustava
Comment[hr]=Calamares Instalacija sustava
Name[ie]=Installar li sistema
Icon[ie]=calamares
GenericName[ie]=Installator del sistema
Comment[ie]=Calamares Installator del sistema
Name[hu]=Rendszer telepítése
Icon[hu]=calamares
GenericName[hu]=Rendszertelepítő
@ -184,6 +188,10 @@ Name[sv]=Installera system
Icon[sv]=calamares
GenericName[sv]=Systeminstallerare
Comment[sv]=Calamares Systeminstallerare
Name[tg]=Насбкунии низом
Icon[tg]=calamares
GenericName[tg]=Насбкунандаи низом
Comment[tg]=Calamares Насбкунандаи низом
Name[th]=
Name[uk]=Встановити Систему
Icon[uk]=calamares

View File

@ -1,21 +1,13 @@
# Calamares Release Process
> Calamares releases are now rolling when-they-are-ready releases.
> Releases are made from *master* and tagged there. When, in future,
> Releases are made from *calamares* and tagged there. When, in future,
> LTS releases resume, these steps may be edited again.
>
> Most things are automated through the release script [RELEASE.sh](RELEASE.sh)
## (0) A week in advance
## (1) Preparation
* Run [Coverity scan][coverity], fix what's relevant. The Coverity scan runs
automatically once a week on master. The badge is displayed on the
project front page and in the wiki.
* Build with clang -Weverything, fix what's relevant.
```
rm -rf build ; mkdir build ; cd build
CC=clang CXX=clang++ cmake .. && make
```
* Make sure all tests pass.
```
make
@ -25,16 +17,6 @@
an additional environment variable to be set for some tests, which will
destroy an attached disk. This is not always desirable. There are some
sample config-files that are empty and which fail the config-tests.
* Notify [translators][transifex]. In the dashboard there is an *Announcements*
link that you can use to send a translation announcement. Note that regular
use of `txpush.sh` will notify translators as well of any changes.
[coverity]: https://scan.coverity.com/projects/calamares-calamares?tab=overview
[transifex]: https://www.transifex.com/calamares/calamares/dashboard/
## (1) Preparation
* Pull latest translations from Transifex. We only push / pull translations
from master, so longer-lived branches (e.g. 3.1.x) don't get translation
updates. This is to keep the translation workflow simple. The script
@ -47,13 +29,8 @@
fairly complete translations, or use `ci/txstats.py` for an automated
suggestion. If there are changes, commit them.
* Push the changes.
* Drop the RC variable to 0 in `CMakeLists.txt`, *CALAMARES_VERSION_RC*.
* Check `README.md` and the
[Coding Guide](https://github.com/calamares/calamares/wiki/Develop-Code),
make sure it's all still
relevant. Run `ci/calamaresstyle` to check the C++ code style.
Run pycodestyle on recently-modified Python modules, fix what makes sense.
* Check defaults in `settings.conf` and other configuration files.
* Drop the RC variable to 0 in `CMakeLists.txt`, *CALAMARES_VERSION_RC*.
* Edit `CHANGES` and set the date of the release.
* Commit both. This is usually done with commit-message
*Changes: pre-release housekeeping*.
@ -73,44 +50,16 @@
On success, it prints out a suitable signature- and SHA256 blurb
for use in the release announcement.
### (2.1) Buld and Test
## (3) Release
* Build with gcc. If available, build again with Clang and double-check
any warnings Clang produces.
* Run the tests; `make test` in the build directory should have no
failures (or if there are, know why they are there).
Follow the instructions printed by the release script.
### (2.2) Tag
* `git tag -s v1.1.0` Make sure the signing key is known in GitHub, so that the
tag is shown as a verified tag. Do not sign -rc tags.
You can use `make show-version` in the build directory to get the right
version number -- this will fail if you didn't follow step (1).
### (2.3) Tarball
* Create tarball: `git-archive-all -v calamares-1.1-rc1.tar.gz` or without
the helper script,
```
V=calamares-3.1.5
git archive -o $V.tar.gz --prefix $V/ master
```
Double check that the tarball matches the version number.
* Test tarball (e.g. unpack somewhere else and run the tests from step 0).
## (3) Housekeeping
* Generate MD5 and SHA256 checksums.
* Upload tarball.
* Announce on mailing list, notify packagers.
* Write release article.
* Publish tarball.
* Update download page.
* Push the tags.
* Create a new release on GitHub.
* Upload tarball and signature.
* Publish release article on `calamares.io`.
* Publicize on social networks.
* Close associated milestone on GitHub if this is the actual release.
* Publish blog post.
* Close associated milestone on GitHub if it's entirely done.
* Update topic on #calamares IRC channel.
## (4) Post-Release
@ -133,3 +82,44 @@ This release contains contributions from (alphabetically by first name):
## Modules ##
- No module changes yet
```
# Related Material
> This section isn't directly related to any specific release,
> but bears on all releases.
## GPG Key Maintainence
Calamares uses GPG Keys for signing the tarballs and some commits
(tags, mostly). Calamares uses the **maintainer's** personal GPG
key for this. This section details some GPG activities that the
maintainer should do with those keys.
- Signing sub-key. It's convenient to use a signing sub-key specifically
for the signing of Calamares. To do so, add a key to the private key.
It's recommended to use key expiry, and to update signing keys periodically.
- Run `gpg -K` to find the key ID of your personal GPG secret key.
- Run `gpg --edit-key <keyid>` to edit that personal GPG key.
- In gpg edit-mode, use `addkey`, then pick a key type that is *sign-only*
(e.g. type 4, *RSA (sign only)*), then pick a keysize (3072 seems ok
as of 2020) and set a key expiry time, (e.g. in 18 months time).
- After generation, the secret key information is printed again, now
including the new signing subkey:
```
ssb rsa3072/0xCFDDC96F12B1915C
created: 2020-07-11 expires: 2022-01-02 usage: S
```
- Update the `RELEASE.sh` script with a new signing sub-key ID when a new
one is generated. Also announce the change of signing sub-key (e.g. on
the Calmares site or as part of a release announcement).
- Send the updated key to keyservers with `gpg --send-keys <keyid>`
- Optional: sanitize the keyring for use in development machines.
Export the current subkeys of the master key and keep **only** those
secret keys around. There is documentation
[here](https://blog.tinned-software.net/create-gnupg-key-with-sub-keys-to-sign-encrypt-authenticate/)
but be careful.
- Export the public key material with `gpg --export --armor <keyid>`,
possibly also setting an output file.
- Upload that public key to the relevant GitHub profile.
- Upload that public key to the Calamares site.

View File

@ -781,7 +781,7 @@ Instalační program bude ukončen a všechny změny ztraceny.</translation>
<message>
<location filename="../src/modules/welcome/Config.cpp" line="245"/>
<source>&lt;h1&gt;Welcome to the Calamares setup program for %1&lt;/h1&gt;</source>
<translation type="unfinished"/>
<translation>&lt;h1&gt;Vítejte v Calamares instalačním programu pro %1.&lt;/h1&gt;</translation>
</message>
<message>
<location filename="../src/modules/welcome/Config.cpp" line="246"/>
@ -796,7 +796,7 @@ Instalační program bude ukončen a všechny změny ztraceny.</translation>
<message>
<location filename="../src/modules/welcome/Config.cpp" line="252"/>
<source>&lt;h1&gt;Welcome to the %1 installer&lt;/h1&gt;</source>
<translation type="unfinished"/>
<translation>&lt;h1&gt;Vítejte v instalátoru %1.&lt;/h1&gt;</translation>
</message>
</context>
<context>
@ -3424,7 +3424,7 @@ Výstup:
<message>
<location filename="../src/modules/tracking/TrackingJobs.cpp" line="202"/>
<source>KDE user feedback</source>
<translation type="unfinished"/>
<translation>Zpětná vazba uživatele KDE</translation>
</message>
<message>
<location filename="../src/modules/tracking/TrackingJobs.cpp" line="214"/>
@ -3445,7 +3445,7 @@ Výstup:
<message>
<location filename="../src/modules/tracking/TrackingJobs.cpp" line="243"/>
<source>Could not configure KDE user feedback correctly, Calamares error %1.</source>
<translation type="unfinished"/>
<translation>Nepodařilo se správně nastavit zpětnou vazbu KDE uživatele, chyba Calamares %1.</translation>
</message>
</context>
<context>

View File

@ -792,7 +792,7 @@ Dies wird das Installationsprogramm beenden und alle Änderungen gehen verloren.
<message>
<location filename="../src/modules/welcome/Config.cpp" line="252"/>
<source>&lt;h1&gt;Welcome to the %1 installer&lt;/h1&gt;</source>
<translation type="unfinished"/>
<translation>&lt;h1&gt;Willkommen zum %1 Installationsprogramm&lt;/h1&gt;</translation>
</message>
</context>
<context>

View File

@ -0,0 +1,5 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/lang">
@calamares_i18n_qrc_content@
</qresource>
</RCC>

3941
lang/calamares_ie.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -776,22 +776,22 @@ Il programma d'installazione sarà terminato e tutte le modifiche andranno perse
<message>
<location filename="../src/modules/welcome/Config.cpp" line="245"/>
<source>&lt;h1&gt;Welcome to the Calamares setup program for %1&lt;/h1&gt;</source>
<translation type="unfinished"/>
<translation>Benvenuto nel programma di installazione Calamares di %1</translation>
</message>
<message>
<location filename="../src/modules/welcome/Config.cpp" line="246"/>
<source>&lt;h1&gt;Welcome to %1 setup&lt;/h1&gt;</source>
<translation type="unfinished"/>
<translation>Benvenuto nell'installazione di %1</translation>
</message>
<message>
<location filename="../src/modules/welcome/Config.cpp" line="251"/>
<source>&lt;h1&gt;Welcome to the Calamares installer for %1&lt;/h1&gt;</source>
<translation type="unfinished"/>
<translation>Benvenuto nel programma di installazione Calamares di %1</translation>
</message>
<message>
<location filename="../src/modules/welcome/Config.cpp" line="252"/>
<source>&lt;h1&gt;Welcome to the %1 installer&lt;/h1&gt;</source>
<translation type="unfinished"/>
<translation>Benvenuto nel programma di installazione di %1</translation>
</message>
</context>
<context>
@ -1780,7 +1780,7 @@ Il programma d'installazione sarà terminato e tutte le modifiche andranno perse
<source>Please select your preferred location on the map so the installer can suggest the locale
and timezone settings for you. You can fine-tune the suggested settings below. Search the map by dragging
to move and using the +/- buttons to zoom in/out or use mouse scrolling for zooming.</source>
<translation type="unfinished"/>
<translation>Seleziona la tua posizione sulla mappa in modo che il programma di installazione possa suggerirti la localizzazione e le impostazioni del fuso orario. Puoi modificare le impostazioni suggerite nella parte in basso. Trascina la mappa per spostarti e usa i pulsanti +/- oppure la rotella del mouse per ingrandire o rimpicciolire.</translation>
</message>
</context>
<context>
@ -1926,12 +1926,12 @@ Il programma d'installazione sarà terminato e tutte le modifiche andranno perse
<message>
<location filename="../src/modules/localeq/Offline.qml" line="62"/>
<source>Timezone: %1</source>
<translation type="unfinished"/>
<translation>Fuso orario: %1</translation>
</message>
<message>
<location filename="../src/modules/localeq/Offline.qml" line="77"/>
<source>To be able to select a timezone, make sure you are connected to the internet. Restart the installer after connecting. You can fine-tune Language and Locale settings below.</source>
<translation type="unfinished"/>
<translation>Per selezionare un fuso orario, assicurati di essere collegato ad Internet. Riavvia il programma di installazione dopo esserti collegato. Puoi modificare le impostazioni relative alla lingua e alla posizione nella parte in basso.</translation>
</message>
</context>
<context>
@ -2836,7 +2836,7 @@ Output:
<location filename="../src/modules/welcomeq/Recommended.qml" line="49"/>
<source>&lt;p&gt;This computer does not satisfy some of the recommended requirements for setting up %1.&lt;br/&gt;
Setup can continue, but some features might be disabled.&lt;/p&gt;</source>
<translation type="unfinished"/>
<translation>Questo computer non soddisfa alcuni requisiti raccomandati per poter installare %1. L'installazione può continuare, ma alcune funzionalità potrebbero essere disabilitate. </translation>
</message>
</context>
<context>
@ -2947,13 +2947,13 @@ Output:
<location filename="../src/modules/welcomeq/Requirements.qml" line="47"/>
<source>&lt;p&gt;This computer does not satisfy the minimum requirements for installing %1.&lt;br/&gt;
Installation cannot continue.&lt;/p&gt;</source>
<translation type="unfinished"/>
<translation>Questo computer non soddisfa i requisiti minimi per poter installare %1. L'installazione non può continuare.</translation>
</message>
<message>
<location filename="../src/modules/welcomeq/Requirements.qml" line="49"/>
<source>&lt;p&gt;This computer does not satisfy some of the recommended requirements for setting up %1.&lt;br/&gt;
Setup can continue, but some features might be disabled.&lt;/p&gt;</source>
<translation type="unfinished"/>
<translation>Questo computer non soddisfa alcuni requisiti raccomandati per poter installare %1. L'installazione può continuare, ma alcune funzionalità potrebbero essere disabilitate. </translation>
</message>
</context>
<context>
@ -3419,12 +3419,12 @@ Output:
<message>
<location filename="../src/modules/tracking/TrackingJobs.cpp" line="202"/>
<source>KDE user feedback</source>
<translation type="unfinished"/>
<translation>Riscontro dell'utente di KDE </translation>
</message>
<message>
<location filename="../src/modules/tracking/TrackingJobs.cpp" line="214"/>
<source>Configuring KDE user feedback.</source>
<translation type="unfinished"/>
<translation>Sto configurando il riscontro dell'utente di KDE</translation>
</message>
<message>
<location filename="../src/modules/tracking/TrackingJobs.cpp" line="236"/>
@ -3865,7 +3865,7 @@ Output:
<message>
<location filename="../src/modules/localeq/localeq.qml" line="98"/>
<source>System language set to %1</source>
<translation type="unfinished"/>
<translation>Lingua di sistema impostata su %1</translation>
</message>
<message>
<location filename="../src/modules/localeq/localeq.qml" line="106"/>

View File

@ -245,7 +245,7 @@
<source>(%n second(s))</source>
<translation>
<numerusform>(%n seconde)</numerusform>
<numerusform>(%n seconden)</numerusform>
<numerusform>(%n seconde(n))</numerusform>
</translation>
</message>
<message>
@ -528,7 +528,7 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan.
<message>
<location filename="../src/modules/partition/gui/ChoicePage.cpp" line="334"/>
<source>&lt;strong&gt;Manual partitioning&lt;/strong&gt;&lt;br/&gt;You can create or resize partitions yourself. Having a GPT partition table and &lt;strong&gt;fat32 512Mb /boot partition is a must for UEFI installs&lt;/strong&gt;, either use an existing without formatting or create one.</source>
<translation type="unfinished"/>
<translation>&lt;strong&gt;Handmatige partitionering&lt;/strong&gt;&lt;br/&gt;Je kunt zelf de partities creëren of wijzigen. Het hebben van een GPT partititie tabel and &lt;strong&gt;een FAT-32 512 MB /boot partitie is belangrijk voor UEFI installaties&lt;/strong&gt;. Gebruik een bestaande zonder formatteren of maak een nieuwe aan.</translation>
</message>
<message>
<location filename="../src/modules/partition/gui/ChoicePage.cpp" line="833"/>
@ -1426,7 +1426,7 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan.
<message>
<location filename="../src/modules/welcome/checker/GeneralRequirements.cpp" line="214"/>
<source>The screen is too small to display the installer.</source>
<translation>Het schem is te klein on het installatieprogramma te vertonen.</translation>
<translation>Het scherm is te klein on het installatieprogramma te laten zien.</translation>
</message>
</context>
<context>
@ -1655,12 +1655,12 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan.
<message>
<location filename="../src/modules/license/LicenseWidget.cpp" line="194"/>
<source>Hide license text</source>
<translation type="unfinished"/>
<translation>Verberg licentietekst</translation>
</message>
<message>
<location filename="../src/modules/license/LicenseWidget.cpp" line="194"/>
<source>Show the license text</source>
<translation type="unfinished"/>
<translation>Toon licentietekst</translation>
</message>
<message>
<location filename="../src/modules/license/LicenseWidget.cpp" line="198"/>
@ -2268,7 +2268,7 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan.
<message>
<location filename="../src/modules/users/page_usersetup.ui" line="51"/>
<source>Your Full Name</source>
<translation>Volledige Naam</translation>
<translation>Volledige naam</translation>
</message>
<message>
<location filename="../src/modules/users/page_usersetup.ui" line="120"/>
@ -2278,7 +2278,7 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan.
<message>
<location filename="../src/modules/users/page_usersetup.ui" line="144"/>
<source>login</source>
<translation type="unfinished"/>
<translation>Gebruikersnaam</translation>
</message>
<message>
<location filename="../src/modules/users/page_usersetup.ui" line="219"/>
@ -2310,13 +2310,13 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan.
<location filename="../src/modules/users/page_usersetup.ui" line="351"/>
<location filename="../src/modules/users/page_usersetup.ui" line="521"/>
<source>Password</source>
<translation type="unfinished"/>
<translation>Wachtwoord</translation>
</message>
<message>
<location filename="../src/modules/users/page_usersetup.ui" line="376"/>
<location filename="../src/modules/users/page_usersetup.ui" line="546"/>
<source>Repeat Password</source>
<translation type="unfinished"/>
<translation>Herhaal wachtwoord</translation>
</message>
<message>
<location filename="../src/modules/users/page_usersetup.ui" line="451"/>

View File

@ -3549,7 +3549,7 @@ Saída:
<message>
<location filename="../src/modules/users/UsersPage.cpp" line="402"/>
<source>Your username must start with a lowercase letter or underscore.</source>
<translation>Seu nome de usuário deve começar com uma letra maiúscula ou com um sublinhado.</translation>
<translation>Seu nome de usuário deve começar com uma letra minúscula ou com um sublinhado.</translation>
</message>
<message>
<location filename="../src/modules/users/UsersPage.cpp" line="409"/>

3947
lang/calamares_tg.ts Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,339 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-06-18 15:42+0200\n"
"PO-Revision-Date: 2017-08-09 10:34+0000\n"
"Language-Team: Interlingue (https://www.transifex.com/calamares/teams/20061/ie/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ie\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/modules/grubcfg/main.py:37
msgid "Configure GRUB."
msgstr ""
#: src/modules/mount/main.py:38
msgid "Mounting partitions."
msgstr ""
#: src/modules/mount/main.py:150 src/modules/initcpiocfg/main.py:205
#: src/modules/initcpiocfg/main.py:209
#: src/modules/luksopenswaphookcfg/main.py:95
#: src/modules/luksopenswaphookcfg/main.py:99 src/modules/rawfs/main.py:173
#: src/modules/initramfscfg/main.py:94 src/modules/initramfscfg/main.py:98
#: src/modules/openrcdmcryptcfg/main.py:78
#: src/modules/openrcdmcryptcfg/main.py:82 src/modules/fstab/main.py:332
#: src/modules/fstab/main.py:338 src/modules/localecfg/main.py:144
#: src/modules/networkcfg/main.py:48
msgid "Configuration Error"
msgstr ""
#: src/modules/mount/main.py:151 src/modules/initcpiocfg/main.py:206
#: src/modules/luksopenswaphookcfg/main.py:96 src/modules/rawfs/main.py:174
#: src/modules/initramfscfg/main.py:95 src/modules/openrcdmcryptcfg/main.py:79
#: src/modules/fstab/main.py:333
msgid "No partitions are defined for <pre>{!s}</pre> to use."
msgstr ""
#: src/modules/services-systemd/main.py:35
msgid "Configure systemd services"
msgstr ""
#: src/modules/services-systemd/main.py:68
#: src/modules/services-openrc/main.py:102
msgid "Cannot modify service"
msgstr ""
#: src/modules/services-systemd/main.py:69
msgid ""
"<code>systemctl {arg!s}</code> call in chroot returned error code {num!s}."
msgstr ""
#: src/modules/services-systemd/main.py:72
#: src/modules/services-systemd/main.py:76
msgid "Cannot enable systemd service <code>{name!s}</code>."
msgstr ""
#: src/modules/services-systemd/main.py:74
msgid "Cannot enable systemd target <code>{name!s}</code>."
msgstr ""
#: src/modules/services-systemd/main.py:78
msgid "Cannot disable systemd target <code>{name!s}</code>."
msgstr ""
#: src/modules/services-systemd/main.py:80
msgid "Cannot mask systemd unit <code>{name!s}</code>."
msgstr ""
#: src/modules/services-systemd/main.py:82
msgid ""
"Unknown systemd commands <code>{command!s}</code> and "
"<code>{suffix!s}</code> for unit {name!s}."
msgstr ""
#: src/modules/umount/main.py:40
msgid "Unmount file systems."
msgstr ""
#: src/modules/unpackfs/main.py:44
msgid "Filling up filesystems."
msgstr ""
#: src/modules/unpackfs/main.py:257
msgid "rsync failed with error code {}."
msgstr ""
#: src/modules/unpackfs/main.py:302
msgid "Unpacking image {}/{}, file {}/{}"
msgstr ""
#: src/modules/unpackfs/main.py:317
msgid "Starting to unpack {}"
msgstr ""
#: src/modules/unpackfs/main.py:326 src/modules/unpackfs/main.py:448
msgid "Failed to unpack image \"{}\""
msgstr ""
#: src/modules/unpackfs/main.py:415
msgid "No mount point for root partition"
msgstr ""
#: src/modules/unpackfs/main.py:416
msgid "globalstorage does not contain a \"rootMountPoint\" key, doing nothing"
msgstr ""
#: src/modules/unpackfs/main.py:421
msgid "Bad mount point for root partition"
msgstr ""
#: src/modules/unpackfs/main.py:422
msgid "rootMountPoint is \"{}\", which does not exist, doing nothing"
msgstr ""
#: src/modules/unpackfs/main.py:438 src/modules/unpackfs/main.py:442
#: src/modules/unpackfs/main.py:462
msgid "Bad unsquash configuration"
msgstr ""
#: src/modules/unpackfs/main.py:439
msgid "The filesystem for \"{}\" ({}) is not supported by your current kernel"
msgstr ""
#: src/modules/unpackfs/main.py:443
msgid "The source filesystem \"{}\" does not exist"
msgstr ""
#: src/modules/unpackfs/main.py:449
msgid ""
"Failed to find unsquashfs, make sure you have the squashfs-tools package "
"installed"
msgstr ""
#: src/modules/unpackfs/main.py:463
msgid "The destination \"{}\" in the target system is not a directory"
msgstr ""
#: src/modules/displaymanager/main.py:523
msgid "Cannot write KDM configuration file"
msgstr ""
#: src/modules/displaymanager/main.py:524
msgid "KDM config file {!s} does not exist"
msgstr ""
#: src/modules/displaymanager/main.py:585
msgid "Cannot write LXDM configuration file"
msgstr ""
#: src/modules/displaymanager/main.py:586
msgid "LXDM config file {!s} does not exist"
msgstr ""
#: src/modules/displaymanager/main.py:669
msgid "Cannot write LightDM configuration file"
msgstr ""
#: src/modules/displaymanager/main.py:670
msgid "LightDM config file {!s} does not exist"
msgstr ""
#: src/modules/displaymanager/main.py:744
msgid "Cannot configure LightDM"
msgstr ""
#: src/modules/displaymanager/main.py:745
msgid "No LightDM greeter installed."
msgstr ""
#: src/modules/displaymanager/main.py:776
msgid "Cannot write SLIM configuration file"
msgstr ""
#: src/modules/displaymanager/main.py:777
msgid "SLIM config file {!s} does not exist"
msgstr ""
#: src/modules/displaymanager/main.py:903
msgid "No display managers selected for the displaymanager module."
msgstr ""
#: src/modules/displaymanager/main.py:904
msgid ""
"The displaymanagers list is empty or undefined in bothglobalstorage and "
"displaymanager.conf."
msgstr ""
#: src/modules/displaymanager/main.py:986
msgid "Display manager configuration was incomplete"
msgstr ""
#: src/modules/initcpiocfg/main.py:37
msgid "Configuring mkinitcpio."
msgstr ""
#: src/modules/initcpiocfg/main.py:210
#: src/modules/luksopenswaphookcfg/main.py:100
#: src/modules/initramfscfg/main.py:99 src/modules/openrcdmcryptcfg/main.py:83
#: src/modules/fstab/main.py:339 src/modules/localecfg/main.py:145
#: src/modules/networkcfg/main.py:49
msgid "No root mount point is given for <pre>{!s}</pre> to use."
msgstr ""
#: src/modules/luksopenswaphookcfg/main.py:35
msgid "Configuring encrypted swap."
msgstr ""
#: src/modules/rawfs/main.py:35
msgid "Installing data."
msgstr ""
#: src/modules/services-openrc/main.py:38
msgid "Configure OpenRC services"
msgstr ""
#: src/modules/services-openrc/main.py:66
msgid "Cannot add service {name!s} to run-level {level!s}."
msgstr ""
#: src/modules/services-openrc/main.py:68
msgid "Cannot remove service {name!s} from run-level {level!s}."
msgstr ""
#: src/modules/services-openrc/main.py:70
msgid ""
"Unknown service-action <code>{arg!s}</code> for service {name!s} in run-"
"level {level!s}."
msgstr ""
#: src/modules/services-openrc/main.py:103
msgid ""
"<code>rc-update {arg!s}</code> call in chroot returned error code {num!s}."
msgstr ""
#: src/modules/services-openrc/main.py:110
msgid "Target runlevel does not exist"
msgstr ""
#: src/modules/services-openrc/main.py:111
msgid ""
"The path for runlevel {level!s} is <code>{path!s}</code>, which does not "
"exist."
msgstr ""
#: src/modules/services-openrc/main.py:119
msgid "Target service does not exist"
msgstr ""
#: src/modules/services-openrc/main.py:120
msgid ""
"The path for service {name!s} is <code>{path!s}</code>, which does not "
"exist."
msgstr ""
#: src/modules/plymouthcfg/main.py:36
msgid "Configure Plymouth theme"
msgstr ""
#: src/modules/packages/main.py:59 src/modules/packages/main.py:68
#: src/modules/packages/main.py:78
msgid "Install packages."
msgstr ""
#: src/modules/packages/main.py:66
#, python-format
msgid "Processing packages (%(count)d / %(total)d)"
msgstr ""
#: src/modules/packages/main.py:71
#, python-format
msgid "Installing one package."
msgid_plural "Installing %(num)d packages."
msgstr[0] ""
msgstr[1] ""
#: src/modules/packages/main.py:74
#, python-format
msgid "Removing one package."
msgid_plural "Removing %(num)d packages."
msgstr[0] ""
msgstr[1] ""
#: src/modules/bootloader/main.py:51
msgid "Install bootloader."
msgstr ""
#: src/modules/hwclock/main.py:35
msgid "Setting hardware clock."
msgstr ""
#: src/modules/dracut/main.py:36
msgid "Creating initramfs with dracut."
msgstr ""
#: src/modules/dracut/main.py:58
msgid "Failed to run dracut on the target"
msgstr ""
#: src/modules/dracut/main.py:59
msgid "The exit code was {}"
msgstr ""
#: src/modules/initramfscfg/main.py:41
msgid "Configuring initramfs."
msgstr ""
#: src/modules/openrcdmcryptcfg/main.py:34
msgid "Configuring OpenRC dmcrypt service."
msgstr ""
#: src/modules/fstab/main.py:38
msgid "Writing fstab."
msgstr ""
#: src/modules/dummypython/main.py:44
msgid "Dummy python job."
msgstr ""
#: src/modules/dummypython/main.py:46 src/modules/dummypython/main.py:102
#: src/modules/dummypython/main.py:103
msgid "Dummy python step {}"
msgstr ""
#: src/modules/localecfg/main.py:39
msgid "Configuring locales."
msgstr ""
#: src/modules/networkcfg/main.py:37
msgid "Saving network configuration."
msgstr ""

Binary file not shown.

View File

@ -0,0 +1,343 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Victor Ibragimov <victor.ibragimov@gmail.com>, 2020
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-06-18 15:42+0200\n"
"PO-Revision-Date: 2017-08-09 10:34+0000\n"
"Last-Translator: Victor Ibragimov <victor.ibragimov@gmail.com>, 2020\n"
"Language-Team: Tajik (https://www.transifex.com/calamares/teams/20061/tg/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: tg\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/modules/grubcfg/main.py:37
msgid "Configure GRUB."
msgstr "Танзимоти GRUB."
#: src/modules/mount/main.py:38
msgid "Mounting partitions."
msgstr "Васлкунии қисмҳои диск."
#: src/modules/mount/main.py:150 src/modules/initcpiocfg/main.py:205
#: src/modules/initcpiocfg/main.py:209
#: src/modules/luksopenswaphookcfg/main.py:95
#: src/modules/luksopenswaphookcfg/main.py:99 src/modules/rawfs/main.py:173
#: src/modules/initramfscfg/main.py:94 src/modules/initramfscfg/main.py:98
#: src/modules/openrcdmcryptcfg/main.py:78
#: src/modules/openrcdmcryptcfg/main.py:82 src/modules/fstab/main.py:332
#: src/modules/fstab/main.py:338 src/modules/localecfg/main.py:144
#: src/modules/networkcfg/main.py:48
msgid "Configuration Error"
msgstr "Хатои танзимкунӣ"
#: src/modules/mount/main.py:151 src/modules/initcpiocfg/main.py:206
#: src/modules/luksopenswaphookcfg/main.py:96 src/modules/rawfs/main.py:174
#: src/modules/initramfscfg/main.py:95 src/modules/openrcdmcryptcfg/main.py:79
#: src/modules/fstab/main.py:333
msgid "No partitions are defined for <pre>{!s}</pre> to use."
msgstr ""
#: src/modules/services-systemd/main.py:35
msgid "Configure systemd services"
msgstr ""
#: src/modules/services-systemd/main.py:68
#: src/modules/services-openrc/main.py:102
msgid "Cannot modify service"
msgstr ""
#: src/modules/services-systemd/main.py:69
msgid ""
"<code>systemctl {arg!s}</code> call in chroot returned error code {num!s}."
msgstr ""
#: src/modules/services-systemd/main.py:72
#: src/modules/services-systemd/main.py:76
msgid "Cannot enable systemd service <code>{name!s}</code>."
msgstr ""
#: src/modules/services-systemd/main.py:74
msgid "Cannot enable systemd target <code>{name!s}</code>."
msgstr ""
#: src/modules/services-systemd/main.py:78
msgid "Cannot disable systemd target <code>{name!s}</code>."
msgstr ""
#: src/modules/services-systemd/main.py:80
msgid "Cannot mask systemd unit <code>{name!s}</code>."
msgstr ""
#: src/modules/services-systemd/main.py:82
msgid ""
"Unknown systemd commands <code>{command!s}</code> and "
"<code>{suffix!s}</code> for unit {name!s}."
msgstr ""
#: src/modules/umount/main.py:40
msgid "Unmount file systems."
msgstr ""
#: src/modules/unpackfs/main.py:44
msgid "Filling up filesystems."
msgstr ""
#: src/modules/unpackfs/main.py:257
msgid "rsync failed with error code {}."
msgstr ""
#: src/modules/unpackfs/main.py:302
msgid "Unpacking image {}/{}, file {}/{}"
msgstr ""
#: src/modules/unpackfs/main.py:317
msgid "Starting to unpack {}"
msgstr ""
#: src/modules/unpackfs/main.py:326 src/modules/unpackfs/main.py:448
msgid "Failed to unpack image \"{}\""
msgstr ""
#: src/modules/unpackfs/main.py:415
msgid "No mount point for root partition"
msgstr ""
#: src/modules/unpackfs/main.py:416
msgid "globalstorage does not contain a \"rootMountPoint\" key, doing nothing"
msgstr ""
#: src/modules/unpackfs/main.py:421
msgid "Bad mount point for root partition"
msgstr ""
#: src/modules/unpackfs/main.py:422
msgid "rootMountPoint is \"{}\", which does not exist, doing nothing"
msgstr ""
#: src/modules/unpackfs/main.py:438 src/modules/unpackfs/main.py:442
#: src/modules/unpackfs/main.py:462
msgid "Bad unsquash configuration"
msgstr ""
#: src/modules/unpackfs/main.py:439
msgid "The filesystem for \"{}\" ({}) is not supported by your current kernel"
msgstr ""
#: src/modules/unpackfs/main.py:443
msgid "The source filesystem \"{}\" does not exist"
msgstr ""
#: src/modules/unpackfs/main.py:449
msgid ""
"Failed to find unsquashfs, make sure you have the squashfs-tools package "
"installed"
msgstr ""
#: src/modules/unpackfs/main.py:463
msgid "The destination \"{}\" in the target system is not a directory"
msgstr ""
#: src/modules/displaymanager/main.py:523
msgid "Cannot write KDM configuration file"
msgstr ""
#: src/modules/displaymanager/main.py:524
msgid "KDM config file {!s} does not exist"
msgstr ""
#: src/modules/displaymanager/main.py:585
msgid "Cannot write LXDM configuration file"
msgstr ""
#: src/modules/displaymanager/main.py:586
msgid "LXDM config file {!s} does not exist"
msgstr ""
#: src/modules/displaymanager/main.py:669
msgid "Cannot write LightDM configuration file"
msgstr ""
#: src/modules/displaymanager/main.py:670
msgid "LightDM config file {!s} does not exist"
msgstr ""
#: src/modules/displaymanager/main.py:744
msgid "Cannot configure LightDM"
msgstr ""
#: src/modules/displaymanager/main.py:745
msgid "No LightDM greeter installed."
msgstr ""
#: src/modules/displaymanager/main.py:776
msgid "Cannot write SLIM configuration file"
msgstr ""
#: src/modules/displaymanager/main.py:777
msgid "SLIM config file {!s} does not exist"
msgstr ""
#: src/modules/displaymanager/main.py:903
msgid "No display managers selected for the displaymanager module."
msgstr ""
#: src/modules/displaymanager/main.py:904
msgid ""
"The displaymanagers list is empty or undefined in bothglobalstorage and "
"displaymanager.conf."
msgstr ""
#: src/modules/displaymanager/main.py:986
msgid "Display manager configuration was incomplete"
msgstr ""
#: src/modules/initcpiocfg/main.py:37
msgid "Configuring mkinitcpio."
msgstr ""
#: src/modules/initcpiocfg/main.py:210
#: src/modules/luksopenswaphookcfg/main.py:100
#: src/modules/initramfscfg/main.py:99 src/modules/openrcdmcryptcfg/main.py:83
#: src/modules/fstab/main.py:339 src/modules/localecfg/main.py:145
#: src/modules/networkcfg/main.py:49
msgid "No root mount point is given for <pre>{!s}</pre> to use."
msgstr ""
#: src/modules/luksopenswaphookcfg/main.py:35
msgid "Configuring encrypted swap."
msgstr ""
#: src/modules/rawfs/main.py:35
msgid "Installing data."
msgstr ""
#: src/modules/services-openrc/main.py:38
msgid "Configure OpenRC services"
msgstr ""
#: src/modules/services-openrc/main.py:66
msgid "Cannot add service {name!s} to run-level {level!s}."
msgstr ""
#: src/modules/services-openrc/main.py:68
msgid "Cannot remove service {name!s} from run-level {level!s}."
msgstr ""
#: src/modules/services-openrc/main.py:70
msgid ""
"Unknown service-action <code>{arg!s}</code> for service {name!s} in run-"
"level {level!s}."
msgstr ""
#: src/modules/services-openrc/main.py:103
msgid ""
"<code>rc-update {arg!s}</code> call in chroot returned error code {num!s}."
msgstr ""
#: src/modules/services-openrc/main.py:110
msgid "Target runlevel does not exist"
msgstr ""
#: src/modules/services-openrc/main.py:111
msgid ""
"The path for runlevel {level!s} is <code>{path!s}</code>, which does not "
"exist."
msgstr ""
#: src/modules/services-openrc/main.py:119
msgid "Target service does not exist"
msgstr ""
#: src/modules/services-openrc/main.py:120
msgid ""
"The path for service {name!s} is <code>{path!s}</code>, which does not "
"exist."
msgstr ""
#: src/modules/plymouthcfg/main.py:36
msgid "Configure Plymouth theme"
msgstr ""
#: src/modules/packages/main.py:59 src/modules/packages/main.py:68
#: src/modules/packages/main.py:78
msgid "Install packages."
msgstr ""
#: src/modules/packages/main.py:66
#, python-format
msgid "Processing packages (%(count)d / %(total)d)"
msgstr ""
#: src/modules/packages/main.py:71
#, python-format
msgid "Installing one package."
msgid_plural "Installing %(num)d packages."
msgstr[0] ""
msgstr[1] ""
#: src/modules/packages/main.py:74
#, python-format
msgid "Removing one package."
msgid_plural "Removing %(num)d packages."
msgstr[0] ""
msgstr[1] ""
#: src/modules/bootloader/main.py:51
msgid "Install bootloader."
msgstr ""
#: src/modules/hwclock/main.py:35
msgid "Setting hardware clock."
msgstr ""
#: src/modules/dracut/main.py:36
msgid "Creating initramfs with dracut."
msgstr ""
#: src/modules/dracut/main.py:58
msgid "Failed to run dracut on the target"
msgstr ""
#: src/modules/dracut/main.py:59
msgid "The exit code was {}"
msgstr ""
#: src/modules/initramfscfg/main.py:41
msgid "Configuring initramfs."
msgstr ""
#: src/modules/openrcdmcryptcfg/main.py:34
msgid "Configuring OpenRC dmcrypt service."
msgstr ""
#: src/modules/fstab/main.py:38
msgid "Writing fstab."
msgstr ""
#: src/modules/dummypython/main.py:44
msgid "Dummy python job."
msgstr ""
#: src/modules/dummypython/main.py:46 src/modules/dummypython/main.py:102
#: src/modules/dummypython/main.py:103
msgid "Dummy python step {}"
msgstr ""
#: src/modules/localecfg/main.py:39
msgid "Configuring locales."
msgstr ""
#: src/modules/networkcfg/main.py:37
msgid "Saving network configuration."
msgstr ""

View File

@ -34,9 +34,8 @@ include_directories(
# Translations
include( CalamaresAddTranslations )
add_calamares_translations( ${CALAMARES_TRANSLATION_LANGUAGES} )
qt5_add_resources( calamaresRc calamares.qrc )
add_executable( calamares_bin ${calamaresSources} ${calamaresRc} ${trans_outfile} )
add_executable( calamares_bin ${calamaresSources} calamares.qrc ${trans_outfile} )
target_include_directories( calamares_bin PRIVATE ${CMAKE_SOURCE_DIR} )
set_target_properties(calamares_bin
PROPERTIES
@ -45,6 +44,7 @@ set_target_properties(calamares_bin
)
calamares_automoc( calamares_bin )
calamares_autouic( calamares_bin )
calamares_autorcc( calamares_bin )
target_link_libraries( calamares_bin
PRIVATE

View File

@ -27,8 +27,10 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} )
configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/CalamaresConfig.h.in
${CMAKE_CURRENT_BINARY_DIR}/CalamaresConfig.h )
configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/../calamares/CalamaresVersion.h.in
configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/CalamaresVersion.h.in
${CMAKE_CURRENT_BINARY_DIR}/CalamaresVersion.h )
configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/CalamaresVersionX.h.in
${CMAKE_CURRENT_BINARY_DIR}/CalamaresVersionX.h )
set( OPTIONAL_PRIVATE_LIBRARIES "" )
set( OPTIONAL_PUBLIC_LIBRARIES "" )
@ -76,6 +78,7 @@ set( libSources
utils/Dirs.cpp
utils/Entropy.cpp
utils/Logger.cpp
utils/Permissions.cpp
utils/PluginFactory.cpp
utils/Retranslator.cpp
utils/String.cpp

View File

@ -4,7 +4,7 @@
#cmakedefine CALAMARES_ORGANIZATION_NAME "${CALAMARES_ORGANIZATION_NAME}"
#cmakedefine CALAMARES_ORGANIZATION_DOMAIN "${CALAMARES_ORGANIZATION_DOMAIN}"
#cmakedefine CALAMARES_APPLICATION_NAME "${CALAMARES_APPLICATION_NAME}"
#cmakedefine CALAMARES_VERSION "${CALAMARES_VERSION}"
#cmakedefine CALAMARES_VERSION "${CALAMARES_VERSION_SHORT}"
#cmakedefine CALAMARES_VERSION_SHORT "${CALAMARES_VERSION_SHORT}"
#cmakedefine CALAMARES_VERSION_MAJOR "${CALAMARES_VERSION_MAJOR}"

View File

@ -0,0 +1,13 @@
// Same as CalamaresVersion.h, but with a full-git-extended VERSION
// rather than the short (vM.m.p) semantic version.
#ifndef CALAMARES_VERSION_H
// On purpose, do not define the guard, but let CalamaresVersion.h do it
// #define CALAMARES_VERSION_H
#include "CalamaresVersion.h"
#undef CALAMARES_VERSION
#cmakedefine CALAMARES_VERSION "${CALAMARES_VERSION}"
#endif // CALAMARES_VERSION_H

View File

@ -36,8 +36,8 @@ using CalamaresUtils::operator""_MiB;
namespace Calamares
{
GlobalStorage::GlobalStorage()
: QObject( nullptr )
GlobalStorage::GlobalStorage( QObject* parent )
: QObject( parent )
{
}

View File

@ -39,7 +39,7 @@ class GlobalStorage : public QObject
{
Q_OBJECT
public:
explicit GlobalStorage();
explicit GlobalStorage( QObject* parent = nullptr );
//NOTE: thread safety is guaranteed by JobQueue, which executes jobs one by one.
// If at any time jobs become concurrent, this class must be made thread-safe.

View File

@ -170,7 +170,7 @@ JobQueue::globalStorage() const
JobQueue::JobQueue( QObject* parent )
: QObject( parent )
, m_thread( new JobThread( this ) )
, m_storage( new GlobalStorage() )
, m_storage( new GlobalStorage( this ) )
{
Q_ASSERT( !s_instance );
s_instance = this;

View File

@ -1,6 +1,7 @@
/* === 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
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -15,8 +16,6 @@
* You should have received a copy of the GNU General Public License
* along with Calamares. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
*/
@ -88,7 +87,13 @@ public:
}
};
/// @brief A pair of strings for timezone regions (e.g. "America")
/** @brief Timezone regions (e.g. "America")
*
* A region has a key and a human-readable name, but also
* a collection of associated timezone zones (TZZone, below).
* This class is not usually constructed, but uses fromFile()
* to load a complete tree structure of timezones.
*/
class TZRegion : public CStringPair
{
Q_OBJECT
@ -120,7 +125,12 @@ private:
CStringPairList m_zones;
};
/// @brief A pair of strings for specific timezone names (e.g. "New_York")
/** @brief Specific timezone zones (e.g. "New_York", "New York")
*
* A timezone zone lives in a region, and has some associated
* data like the country (used to map likely languages) and latitude
* and longitude information.
*/
class TZZone : public CStringPair
{
Q_OBJECT

View File

@ -168,7 +168,11 @@ Manager::checkHasInternet()
{
hasInternet = synchronousPing( d->m_hasInternetUrl );
}
d->m_hasInternet = hasInternet;
if ( hasInternet != d->m_hasInternet )
{
d->m_hasInternet = hasInternet;
emit hasInternetChanged( hasInternet );
}
return hasInternet;
}

View File

@ -98,9 +98,10 @@ struct RequestStatus
QDebug& operator<<( QDebug& s, const RequestStatus& e );
class DLLEXPORT Manager : QObject
class DLLEXPORT Manager : public QObject
{
Q_OBJECT
Q_PROPERTY( bool hasInternet READ hasInternet NOTIFY hasInternetChanged FINAL )
Manager();
@ -133,6 +134,16 @@ public:
/// @brief Set the URL which is used for the general "is there internet" check.
void setCheckHasInternetUrl( const QUrl& url );
/** @brief Do a network request asynchronously.
*
* Returns a pointer to the reply-from-the-request.
* This may be a nullptr if an error occurs immediately.
* The caller is responsible for cleaning up the reply (eventually).
*/
QNetworkReply* asynchronousGet( const QUrl& url, const RequestOptions& options = RequestOptions() );
public Q_SLOTS:
/** @brief Do an explicit check for internet connectivity.
*
* This **may** do a ping to the configured check URL, but can also
@ -148,13 +159,13 @@ public:
*/
bool hasInternet();
/** @brief Do a network request asynchronously.
signals:
/** @brief Indicates that internet connectivity status has changed
*
* Returns a pointer to the reply-from-the-request.
* This may be a nullptr if an error occurs immediately.
* The caller is responsible for cleaning up the reply (eventually).
* The value is that returned from hasInternet() -- @c true when there
* is connectivity, @c false otherwise.
*/
QNetworkReply* asynchronousGet( const QUrl& url, const RequestOptions& options = RequestOptions() );
void hasInternetChanged( bool );
private:
class Private;

View File

@ -3,6 +3,7 @@
* SPDX-FileCopyrightText: 2010-2011 Christian Muehlhaeuser <muesli@tomahawk-player.org>
* SPDX-FileCopyrightText: 2014 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
*
* Calamares is free software: you can redistribute it and/or modify
@ -18,15 +19,12 @@
* You should have received a copy of the GNU General Public License
* along with Calamares. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
*/
#include "Logger.h"
#include <fstream>
#include <iostream>
#include "CalamaresVersionX.h"
#include "utils/Dirs.h"
#include <QCoreApplication>
#include <QDir>
@ -35,10 +33,10 @@
#include <QTime>
#include <QVariant>
#include "CalamaresVersion.h"
#include "utils/Dirs.h"
#include <fstream>
#include <iostream>
#define LOGFILE_SIZE 1024 * 256
static constexpr const int LOGFILE_SIZE = 1024 * 256;
static std::ofstream logfile;
static unsigned int s_threshold =

View File

@ -0,0 +1,124 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2018 Scott Harvey <scott@spharvey.me>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
*/
#include "Permissions.h"
#include "Logger.h"
#include <QProcess>
#include <QString>
#include <QStringList>
#include <sys/stat.h>
namespace CalamaresUtils
{
Permissions::Permissions()
: m_username()
, m_group()
, m_value( 0 )
, m_valid( false )
{
}
Permissions::Permissions( QString const& p )
: Permissions()
{
parsePermissions( p );
}
void
Permissions::parsePermissions( QString const& p )
{
QStringList segments = p.split( ":" );
if ( segments.length() != 3 )
{
m_valid = false;
return;
}
if ( segments[ 0 ].isEmpty() || segments[ 1 ].isEmpty() )
{
m_valid = false;
return;
}
bool ok;
int octal = segments[ 2 ].toInt( &ok, 8 );
if ( !ok || octal == 0 )
{
m_valid = false;
return;
}
else
{
m_value = octal;
}
// We have exactly three segments and the third is valid octal,
// so we can declare the string valid and set the user and group names
m_valid = true;
m_username = segments[ 0 ];
m_group = segments[ 1 ];
return;
}
bool
Permissions::apply( const QString& path, int mode )
{
// We **don't** use QFile::setPermissions() here because it takes
// a Qt flags object that subtlely does not align with POSIX bits.
// The Qt flags are **hex** based, so 0x755 for rwxr-xr-x, while
// our integer (mode_t) stores **octal** based flags.
//
// Call chmod(2) directly, that's what Qt would be doing underneath
// anyway.
int r = chmod( path.toUtf8().constData(), mode_t( mode ) );
if ( r )
{
cDebug() << Logger::SubEntry << "Could not set permissions of" << path << "to" << QString::number( mode, 8 );
}
return r == 0;
}
bool
Permissions::apply( const QString& path, const CalamaresUtils::Permissions& p )
{
if ( !p.isValid() )
{
return false;
}
bool r = apply( path, p.value() );
if ( r )
{
// We don't use chgrp(2) or chown(2) here because then we need to
// go through the users list (which one, target or source?) to get
// uid_t and gid_t values to pass to that system call.
//
// Do a lame cop-out and let the chown(8) utility do the heavy lifting.
if ( QProcess::execute( "chown", { p.username() + ':' + p.group(), path } ) )
{
r = false;
cDebug() << Logger::SubEntry << "Could not set owner of" << path << "to"
<< ( p.username() + ':' + p.group() );
}
}
if ( r )
{
/* NOTUSED */ apply( path, p.value() );
}
return r;
}
} // namespace CalamaresUtils

View File

@ -0,0 +1,92 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2018 Scott Harvey <scott@spharvey.me>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
*/
#ifndef LIBCALAMARES_PERMISSIONS_H
#define LIBCALAMARES_PERMISSIONS_H
#include "DllMacro.h"
#include <QString>
namespace CalamaresUtils
{
/**
* @brief The Permissions class takes a QString @p in the form of
* <user>:<group>:<permissions>, checks it for validity, and makes the three
* components available indivdually.
*/
class DLLEXPORT Permissions
{
public:
/** @brief Constructor
*
* Splits the string @p at the colon (":") into separate elements for
* <user>, <group>, and <value> (permissions), where <value> is interpreted
* as an **octal** integer. That is, "root:wheel:755" will give
* you an integer value of four-hundred-ninety-three (493),
* corresponding to the UNIX file permissions rwxr-xr-x,
* as one would expect from chmod and other command-line utilities.
*/
Permissions( QString const& p );
/// @brief Default constructor of an invalid Permissions.
Permissions();
/// @brief Was the Permissions object constructed from valid data?
bool isValid() const { return m_valid; }
/// @brief The user (first component, e.g. "root" in "root:wheel:755")
QString username() const { return m_username; }
/// @brief The group (second component, e.g. "wheel" in "root:wheel:755")
QString group() const { return m_group; }
/** @brief The value (file permission) as an integer.
*
* Bear in mind that input is in octal, but integers are just integers;
* naively printing them will get decimal results (e.g. 493 from the
* input of "root:wheel:755"). This is suitable to pass to apply().
*/
int value() const { return m_value; }
/** @brief The value (file permission) as octal string
*
* This is suitable for passing to chmod-the-program, or for
* recreating the original Permissions string.
*/
QString octal() const { return QString::number( value(), 8 ); }
/** @brief Sets the file-access @p mode of @p path
*
* Pass a path that is relative (or absolute) in the **host** system.
*/
static bool apply( const QString& path, int mode );
/** @brief Do both chmod and chown on @p path
*
* Note that interpreting user- and group- names for applying the
* permissions can be different between the host system and the target
* system; the target might not have a "live" user, for instance, and
* the host won't have the user-entered username for the installation.
*
* For this call, the names are interpreted in the **host** system.
* Pass a path that is relative (or absolute) in the **host** system.
*/
static bool apply( const QString& path, const Permissions& p );
/// Convenience method for apply(const QString&, const Permissions& )
bool apply( const QString& path ) const { return apply( path, *this ); }
private:
void parsePermissions( QString const& p );
QString m_username;
QString m_group;
int m_value;
bool m_valid;
};
} // namespace CalamaresUtils
#endif // LIBCALAMARES_PERMISSIONS_H

View File

@ -24,6 +24,7 @@
#define UTILS_RAII_H
#include <QObject>
#include <QSignalBlocker>
#include <type_traits>
@ -44,4 +45,21 @@ struct cqDeleter
}
};
/// @brief Sets a bool to @p value and resets to !value on destruction
template < bool value >
struct cBoolSetter
{
bool& m_b;
cBoolSetter( bool& b )
: m_b( b )
{
m_b = value;
}
~cBoolSetter() { m_b = !value; }
};
/// @brief Blocks signals on a QObject until destruction
using cSignalBlocker = QSignalBlocker;
#endif

View File

@ -103,11 +103,37 @@ public:
RequirementsModel* requirementsModel() { return m_requirementsModel; }
signals:
/** @brief Emitted when all the module **configuration** has been read
*
* This indicates that all of the module.desc files have been
* loaded; bad ones are silently skipped, so this just indicates
* that the module manager is ready for the next phase (loading).
*/
void initDone();
void modulesLoaded(); /// All of the modules were loaded successfully
void modulesFailed( QStringList ); /// .. or not
// Below, see RequirementsChecker documentation
void requirementsComplete( bool );
/** @brief Emitted when all the modules are loaded successfully
*
* Each module listed in the settings is loaded. Modules are loaded
* only once, even when instantiated multiple times. If all of
* the listed modules are successfully loaded, this signal is
* emitted (otherwise, it isn't, so you need to wait for **both**
* of the signals).
*
* If this is emitted (i.e. all modules have loaded) then the next
* phase, requirements checking, can be started.
*/
void modulesLoaded();
/** @brief Emitted if any modules failed to load
*
* Modules that failed to load (for any reason) are listed by
* instance key (e.g. "welcome@welcome", "shellprocess@mycustomthing").
*/
void modulesFailed( QStringList );
/** @brief Emitted after all requirements have been checked
*
* The bool @p canContinue indicates if all of the **mandatory** requirements
* are satisfied (e.g. whether installation can continue).
*/
void requirementsComplete( bool canContinue );
private slots:
void doInit();

View File

@ -23,6 +23,7 @@
#include "JobQueue.h"
#include "Settings.h"
#include "ViewManager.h"
#include "network/Manager.h"
#include "utils/Dirs.h"
#include "utils/Logger.h"
@ -242,6 +243,10 @@ registerQmlModels()
"io.calamares.core", 1, 0, "Global", []( QQmlEngine*, QJSEngine* ) -> QObject* {
return Calamares::JobQueue::instance()->globalStorage();
} );
qmlRegisterSingletonType< CalamaresUtils::Network::Manager >(
"io.calamares.core", 1, 0, "Network", []( QQmlEngine*, QJSEngine* ) -> QObject* {
return &CalamaresUtils::Network::Manager::instance();
} );
}
}

View File

@ -15,7 +15,7 @@ ListView {
z: parent.z - 1
anchors.fill: parent
color: Kirigami.Theme.backgroundColor
color: "#BDC3C7"
radius: 5
opacity: 0.7
}

View File

@ -13,8 +13,8 @@ Page {
width: 800 //parent.width
height: 550 //parent.height
Kirigami.Theme.backgroundColor: "#fafafa"
Kirigami.Theme.textColor: "#333"
Kirigami.Theme.backgroundColor: "#FAFAFA"
Kirigami.Theme.textColor: "#1F1F1F"
property string subtitle
property string message
@ -22,39 +22,6 @@ Page {
default property alias content : _content.data
property alias stackView: _stackView
background: Item {
id: _background
Image {
id: _wallpaper
height: parent.height
width: parent.width
sourceSize.width: 800
sourceSize.height: 550
fillMode: Image.PreserveAspectCrop
antialiasing: false
smooth: false
asynchronous: true
cache: true
source: "keyboard.jpg"
}
FastBlur {
id: fastBlur
anchors.fill: parent
source: _wallpaper
radius: 32
transparentBorder: false
cached: true
}
}
ColumnLayout {
id: _content
@ -72,7 +39,7 @@ Page {
wrapMode: Text.NoWrap
elide: Text.ElideMiddle
text: control.title
color: "white"
color: Kirigami.Theme.textColor
font.bold: true
font.weight: Font.Bold
font.pointSize: 24
@ -86,7 +53,7 @@ Page {
wrapMode: Text.Wrap
elide: Text.ElideMiddle
text: control.subtitle
color: "white"
color: Kirigami.Theme.textColor
font.weight: Font.Light
font.pointSize: 12
}
@ -99,7 +66,7 @@ Page {
wrapMode: Text.Wrap
elide: Text.ElideMiddle
text: control.message
color: "white"
color: Kirigami.Theme.textColor
font.weight: Font.Light
font.pointSize: 10
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

View File

@ -182,14 +182,15 @@ ResponsiveBase {
Layout.maximumWidth: 500
Layout.fillWidth: true
Layout.alignment: Qt.AlignCenter
color: control.Kirigami.Theme.textColor
background:Rectangle {
z: parent.z - 1
anchors.fill: parent
color: control.Kirigami.Theme.backgroundColor
color: "#BDC3C7"
radius: 5
opacity: 0.8
opacity: 0.3
}
}
}

View File

@ -6,6 +6,5 @@
<file>ListItemDelegate.qml</file>
<file>ListViewTemplate.qml</file>
<file>ResponsiveBase.qml</file>
<file>keyboard.jpg</file>
</qresource>
</RCC>

View File

@ -1,7 +1,8 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019-2020, Adriaan de Groot <groot@kde.org>
* Copyright 2020, Camilo Higuita <milo.h@aol.com>
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -19,85 +20,43 @@
#include "Config.h"
#include "LCLocaleDialog.h"
#include "SetTimezoneJob.h"
#include "timezonewidget/timezonewidget.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "Settings.h"
#include "locale/Label.h"
#include "locale/TimeZone.h"
#include "utils/CalamaresUtilsGui.h"
#include "modulesystem/ModuleManager.h"
#include "network/Manager.h"
#include "utils/Logger.h"
#include "utils/Retranslator.h"
#include "utils/Variant.h"
#include <QDebug>
#include <QFile>
#include <QProcess>
#include <QTimeZone>
Config::Config( QObject* parent )
: QObject( parent )
, m_regionList( CalamaresUtils::Locale::TZRegion::fromZoneTab() )
, m_regionModel( new CalamaresUtils::Locale::CStringListModel( m_regionList ) )
, m_zonesModel( new CalamaresUtils::Locale::CStringListModel() )
, m_blockTzWidgetSet( false )
/** @brief Load supported locale keys
*
* If i18n/SUPPORTED exists, read the lines from that and return those
* as supported locales; otherwise, try the file at @p localeGenPath
* and get lines from that. Failing both, try the output of `locale -a`.
*
* This gives us a list of locale identifiers (e.g. en_US.UTF-8), which
* are not particularly human-readable.
*
* Only UTF-8 locales are returned (even if the system claims to support
* other, non-UTF-8, locales).
*/
static QStringList
loadLocales( const QString& localeGenPath )
{
connect( m_regionModel, &CalamaresUtils::Locale::CStringListModel::currentIndexChanged, [&]() {
m_zonesModel->setList( static_cast< const CalamaresUtils::Locale::TZRegion* >(
m_regionModel->item( m_regionModel->currentIndex() ) )
->zones() );
updateLocaleLabels();
} );
connect(
m_zonesModel, &CalamaresUtils::Locale::CStringListModel::currentIndexChanged, [&]() { updateLocaleLabels(); } );
}
Config::~Config()
{
qDeleteAll( m_regionList );
}
CalamaresUtils::Locale::CStringListModel*
Config::zonesModel() const
{
return m_zonesModel;
}
CalamaresUtils::Locale::CStringListModel*
Config::regionModel() const
{
return m_regionModel;
}
void
Config::setLocaleInfo( const QString& initialRegion, const QString& initialZone, const QString& localeGenPath )
{
using namespace CalamaresUtils::Locale;
cDebug() << "REGION MODEL SIZE" << initialRegion << initialZone;
auto* region = m_regionList.find< TZRegion >( initialRegion );
if ( region && region->zones().find< TZZone >( initialZone ) )
{
m_regionModel->setCurrentIndex( m_regionModel->indexOf( initialRegion ) );
m_zonesModel->setList( region->zones() );
m_zonesModel->setCurrentIndex( m_zonesModel->indexOf( initialZone ) );
}
else
{
m_regionModel->setCurrentIndex( m_regionModel->indexOf( "America" ) );
m_zonesModel->setList(
static_cast< const TZRegion* >( m_regionModel->item( m_regionModel->currentIndex() ) )->zones() );
m_zonesModel->setCurrentIndex( m_zonesModel->indexOf( "New_York" ) );
}
QStringList localeGenLines;
// Some distros come with a meaningfully commented and easy to parse locale.gen,
// and others ship a separate file /usr/share/i18n/SUPPORTED with a clean list of
// supported locales. We first try that one, and if it doesn't exist, we fall back
// to parsing the lines from locale.gen
m_localeGenLines.clear();
localeGenLines.clear();
QFile supported( "/usr/share/i18n/SUPPORTED" );
QByteArray ba;
@ -109,7 +68,7 @@ Config::setLocaleInfo( const QString& initialRegion, const QString& initialZone,
const auto lines = ba.split( '\n' );
for ( const QByteArray& line : lines )
{
m_localeGenLines.append( QString::fromLatin1( line.simplified() ) );
localeGenLines.append( QString::fromLatin1( line.simplified() ) );
}
}
else
@ -150,11 +109,11 @@ Config::setLocaleInfo( const QString& initialRegion, const QString& initialZone,
continue;
}
m_localeGenLines.append( lineString );
localeGenLines.append( lineString );
}
}
if ( m_localeGenLines.isEmpty() )
if ( localeGenLines.isEmpty() )
{
cWarning() << "cannot acquire a list of available locales."
<< "The locale and localecfg modules will be broken as long as this "
@ -164,168 +123,388 @@ Config::setLocaleInfo( const QString& initialRegion, const QString& initialZone,
<< "* a well-formed"
<< ( localeGenPath.isEmpty() ? QLatin1String( "/etc/locale.gen" ) : localeGenPath ) << "\n\tOR"
<< "* a complete pre-compiled locale-gen database which allows complete locale -a output.";
return; // something went wrong and there's nothing we can do about it.
return localeGenLines; // something went wrong and there's nothing we can do about it.
}
// Assuming we have a list of supported locales, we usually only want UTF-8 ones
// because it's not 1995.
for ( auto it = m_localeGenLines.begin(); it != m_localeGenLines.end(); )
{
if ( !it->contains( "UTF-8", Qt::CaseInsensitive ) && !it->contains( "utf8", Qt::CaseInsensitive ) )
{
it = m_localeGenLines.erase( it );
}
else
{
++it;
}
}
auto notUtf8 = []( const QString& s ) {
return !s.contains( "UTF-8", Qt::CaseInsensitive ) && !s.contains( "utf8", Qt::CaseInsensitive );
};
auto it = std::remove_if( localeGenLines.begin(), localeGenLines.end(), notUtf8 );
localeGenLines.erase( it, localeGenLines.end() );
// We strip " UTF-8" from "en_US.UTF-8 UTF-8" because it's redundant redundant.
for ( auto it = m_localeGenLines.begin(); it != m_localeGenLines.end(); ++it )
{
if ( it->endsWith( " UTF-8" ) )
// Also simplify whitespace.
auto unredundant = []( QString& s ) {
if ( s.endsWith( " UTF-8" ) )
{
it->chop( 6 );
s.chop( 6 );
}
*it = it->simplified();
}
updateGlobalStorage();
updateLocaleLabels();
s = s.simplified();
};
std::for_each( localeGenLines.begin(), localeGenLines.end(), unredundant );
return localeGenLines;
}
static inline const CalamaresUtils::Locale::CStringPairList&
timezoneData()
{
return CalamaresUtils::Locale::TZRegion::fromZoneTab();
}
Config::Config( QObject* parent )
: QObject( parent )
, m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( ::timezoneData() ) )
, m_zonesModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >() )
{
// Slightly unusual: connect to our *own* signals. Wherever the language
// or the location is changed, these signals are emitted, so hook up to
// them to update global storage accordingly. This simplifies code:
// we don't need to call an update-GS method, or introduce an intermediate
// update-thing-and-GS method. And everywhere where we **do** change
// language or location, we already emit the signal.
connect( this, &Config::currentLanguageCodeChanged, [&]() {
auto* gs = Calamares::JobQueue::instance()->globalStorage();
gs->insert( "locale", m_selectedLocaleConfiguration.toBcp47() );
} );
connect( this, &Config::currentLCCodeChanged, [&]() {
auto* gs = Calamares::JobQueue::instance()->globalStorage();
// Update GS localeConf (the LC_ variables)
auto map = localeConfiguration().toMap();
QVariantMap vm;
for ( auto it = map.constBegin(); it != map.constEnd(); ++it )
{
vm.insert( it.key(), it.value() );
}
gs->insert( "localeConf", vm );
} );
connect( this, &Config::currentLocationChanged, [&]() {
auto* gs = Calamares::JobQueue::instance()->globalStorage();
// Update the GS region and zone (and possibly the live timezone)
const auto* location = currentLocation();
bool locationChanged = ( location->region() != gs->value( "locationRegion" ) )
|| ( location->zone() != gs->value( "locationZone" ) );
gs->insert( "locationRegion", location->region() );
gs->insert( "locationZone", location->zone() );
if ( locationChanged && m_adjustLiveTimezone )
{
QProcess::execute( "timedatectl", // depends on systemd
{ "set-timezone", location->region() + '/' + location->zone() } );
}
} );
auto prettyStatusNotify = [&]() { emit prettyStatusChanged( prettyStatus() ); };
connect( this, &Config::currentLanguageStatusChanged, prettyStatusNotify );
connect( this, &Config::currentLCStatusChanged, prettyStatusNotify );
connect( this, &Config::currentLocationStatusChanged, prettyStatusNotify );
}
Config::~Config() {}
const CalamaresUtils::Locale::CStringPairList&
Config::timezoneData() const
{
return ::timezoneData();
}
void
Config::updateGlobalLocale()
Config::setCurrentLocation()
{
auto* gs = Calamares::JobQueue::instance()->globalStorage();
const QString bcp47 = m_selectedLocaleConfiguration.toBcp47();
gs->insert( "locale", bcp47 );
if ( !m_currentLocation && m_startingTimezone.isValid() )
{
setCurrentLocation( m_startingTimezone.first, m_startingTimezone.second );
}
}
void Config::setCurrentLocation(const QString& regionzone)
{
auto r = CalamaresUtils::GeoIP::splitTZString( regionzone );
if ( r.isValid() )
{
setCurrentLocation( r.first, r.second );
}
}
void
Config::updateGlobalStorage()
Config::setCurrentLocation( const QString& regionName, const QString& zoneName )
{
auto* gs = Calamares::JobQueue::instance()->globalStorage();
const auto* location = currentLocation();
bool locationChanged = ( location->region() != gs->value( "locationRegion" ) )
|| ( location->zone() != gs->value( "locationZone" ) );
#ifdef DEBUG_TIMEZONES
if ( locationChanged )
using namespace CalamaresUtils::Locale;
auto* region = timezoneData().find< TZRegion >( regionName );
auto* zone = region ? region->zones().find< TZZone >( zoneName ) : nullptr;
if ( zone )
{
cDebug() << "Location changed" << gs->value( "locationRegion" ) << ',' << gs->value( "locationZone" ) << "to"
<< location->region() << ',' << location->zone();
setCurrentLocation( zone );
}
#endif
gs->insert( "locationRegion", location->region() );
gs->insert( "locationZone", location->zone() );
updateGlobalLocale();
// If we're in chroot mode (normal install mode), then we immediately set the
// timezone on the live system. When debugging timezones, don't bother.
#ifndef DEBUG_TIMEZONES
if ( locationChanged && Calamares::Settings::instance()->doChroot() )
else
{
QProcess::execute( "timedatectl", // depends on systemd
{ "set-timezone", location->region() + '/' + location->zone() } );
// Recursive, but America/New_York always exists.
setCurrentLocation( QStringLiteral( "America" ), QStringLiteral( "New_York" ) );
}
#endif
// Preserve those settings that have been made explicit.
auto newLocale = guessLocaleConfiguration();
if ( !m_selectedLocaleConfiguration.isEmpty() && m_selectedLocaleConfiguration.explicit_lang )
{
newLocale.setLanguage( m_selectedLocaleConfiguration.language() );
}
if ( !m_selectedLocaleConfiguration.isEmpty() && m_selectedLocaleConfiguration.explicit_lc )
{
newLocale.lc_numeric = m_selectedLocaleConfiguration.lc_numeric;
newLocale.lc_time = m_selectedLocaleConfiguration.lc_time;
newLocale.lc_monetary = m_selectedLocaleConfiguration.lc_monetary;
newLocale.lc_paper = m_selectedLocaleConfiguration.lc_paper;
newLocale.lc_name = m_selectedLocaleConfiguration.lc_name;
newLocale.lc_address = m_selectedLocaleConfiguration.lc_address;
newLocale.lc_telephone = m_selectedLocaleConfiguration.lc_telephone;
newLocale.lc_measurement = m_selectedLocaleConfiguration.lc_measurement;
newLocale.lc_identification = m_selectedLocaleConfiguration.lc_identification;
}
newLocale.explicit_lang = m_selectedLocaleConfiguration.explicit_lang;
newLocale.explicit_lc = m_selectedLocaleConfiguration.explicit_lc;
m_selectedLocaleConfiguration = newLocale;
updateLocaleLabels();
}
void
Config::updateLocaleLabels()
Config::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location )
{
LocaleConfiguration lc
= m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration() : m_selectedLocaleConfiguration;
auto labels = prettyLocaleStatus( lc );
emit prettyStatusChanged();
}
if ( location != m_currentLocation )
{
m_currentLocation = location;
// Overwrite those settings that have not been made explicit.
auto newLocale = automaticLocaleConfiguration();
if ( !m_selectedLocaleConfiguration.explicit_lang )
{
m_selectedLocaleConfiguration.setLanguage( newLocale.language() );
emit currentLanguageStatusChanged( currentLanguageStatus() );
}
if ( !m_selectedLocaleConfiguration.explicit_lc )
{
m_selectedLocaleConfiguration.lc_numeric = newLocale.lc_numeric;
m_selectedLocaleConfiguration.lc_time = newLocale.lc_time;
m_selectedLocaleConfiguration.lc_monetary = newLocale.lc_monetary;
m_selectedLocaleConfiguration.lc_paper = newLocale.lc_paper;
m_selectedLocaleConfiguration.lc_name = newLocale.lc_name;
m_selectedLocaleConfiguration.lc_address = newLocale.lc_address;
m_selectedLocaleConfiguration.lc_telephone = newLocale.lc_telephone;
m_selectedLocaleConfiguration.lc_measurement = newLocale.lc_measurement;
m_selectedLocaleConfiguration.lc_identification = newLocale.lc_identification;
std::pair< QString, QString >
Config::prettyLocaleStatus( const LocaleConfiguration& lc ) const
{
using CalamaresUtils::Locale::Label;
Label lang( lc.language(), Label::LabelFormat::AlwaysWithCountry );
Label num( lc.lc_numeric, Label::LabelFormat::AlwaysWithCountry );
return std::make_pair< QString, QString >(
tr( "The system language will be set to %1." ).arg( lang.label() ),
tr( "The numbers and dates locale will be set to %1." ).arg( num.label() ) );
}
Calamares::JobList
Config::createJobs()
{
QList< Calamares::job_ptr > list;
const CalamaresUtils::Locale::TZZone* location = currentLocation();
Calamares::Job* j = new SetTimezoneJob( location->region(), location->zone() );
list.append( Calamares::job_ptr( j ) );
return list;
emit currentLCStatusChanged( currentLCStatus() );
}
emit currentLocationChanged( m_currentLocation );
}
}
LocaleConfiguration
Config::guessLocaleConfiguration() const
Config::automaticLocaleConfiguration() const
{
// Special case: no location has been set at **all**
if ( !currentLocation() )
{
return LocaleConfiguration();
}
return LocaleConfiguration::fromLanguageAndLocation(
QLocale().name(), m_localeGenLines, currentLocation() ? currentLocation()->country() : "" );
QLocale().name(), supportedLocales(), currentLocation()->country() );
}
QMap< QString, QString >
Config::localesMap()
LocaleConfiguration
Config::localeConfiguration() const
{
return m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().toMap()
: m_selectedLocaleConfiguration.toMap();
return m_selectedLocaleConfiguration.isEmpty() ? automaticLocaleConfiguration() : m_selectedLocaleConfiguration;
}
void
Config::setLanguageExplicitly( const QString& language )
{
m_selectedLocaleConfiguration.setLanguage( language );
m_selectedLocaleConfiguration.explicit_lang = true;
emit currentLanguageStatusChanged( currentLanguageStatus() );
emit currentLanguageCodeChanged( currentLanguageCode() );
}
void
Config::setLCLocaleExplicitly( const QString& locale )
{
// TODO: improve the granularity of this setting.
m_selectedLocaleConfiguration.lc_numeric = locale;
m_selectedLocaleConfiguration.lc_time = locale;
m_selectedLocaleConfiguration.lc_monetary = locale;
m_selectedLocaleConfiguration.lc_paper = locale;
m_selectedLocaleConfiguration.lc_name = locale;
m_selectedLocaleConfiguration.lc_address = locale;
m_selectedLocaleConfiguration.lc_telephone = locale;
m_selectedLocaleConfiguration.lc_measurement = locale;
m_selectedLocaleConfiguration.lc_identification = locale;
m_selectedLocaleConfiguration.explicit_lc = true;
emit currentLCStatusChanged( currentLCStatus() );
emit currentLCCodeChanged( currentLCCode() );
}
QString
Config::currentLocationStatus() const
{
return tr( "Set timezone to %1/%2." ).arg( m_currentLocation->region(), m_currentLocation->zone() );
}
static inline QString
localeLabel( const QString& s )
{
using CalamaresUtils::Locale::Label;
Label lang( s, Label::LabelFormat::AlwaysWithCountry );
return lang.label();
}
QString
Config::currentLanguageStatus() const
{
return tr( "The system language will be set to %1." )
.arg( localeLabel( m_selectedLocaleConfiguration.language() ) );
}
QString
Config::currentLCStatus() const
{
return tr( "The numbers and dates locale will be set to %1." )
.arg( localeLabel( m_selectedLocaleConfiguration.lc_numeric ) );
}
QString
Config::prettyStatus() const
{
QString status;
status += tr( "Set timezone to %1/%2.<br/>" )
.arg( m_regionModel->item( m_regionModel->currentIndex() )->tr() )
.arg( m_zonesModel->item( m_zonesModel->currentIndex() )->tr() );
LocaleConfiguration lc
= m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration() : m_selectedLocaleConfiguration;
auto labels = prettyLocaleStatus( lc );
status += labels.first + "<br/>";
status += labels.second + "<br/>";
return status;
QStringList l { currentLocationStatus(), currentLanguageStatus(), currentLCStatus() };
return l.join( QStringLiteral( "<br/>" ) );
}
const CalamaresUtils::Locale::TZZone*
Config::currentLocation() const
static inline void
getLocaleGenLines( const QVariantMap& configurationMap, QStringList& localeGenLines )
{
return static_cast< const CalamaresUtils::Locale::TZZone* >( m_zonesModel->item( m_zonesModel->currentIndex() ) );
QString localeGenPath = CalamaresUtils::getString( configurationMap, "localeGenPath" );
if ( localeGenPath.isEmpty() )
{
localeGenPath = QStringLiteral( "/etc/locale.gen" );
}
localeGenLines = loadLocales( localeGenPath );
}
static inline void
getAdjustLiveTimezone( const QVariantMap& configurationMap, bool& adjustLiveTimezone )
{
adjustLiveTimezone = CalamaresUtils::getBool(
configurationMap, "adjustLiveTimezone", Calamares::Settings::instance()->doChroot() );
#ifdef DEBUG_TIMEZONES
if ( m_adjustLiveTimezone )
{
cWarning() << "Turning off live-timezone adjustments because debugging is on.";
adjustLiveTimezone = false;
}
#endif
#ifdef __FreeBSD__
if ( adjustLiveTimezone )
{
cWarning() << "Turning off live-timezone adjustments on FreeBSD.";
adjustLiveTimezone = false;
}
#endif
}
static inline void
getStartingTimezone( const QVariantMap& configurationMap, CalamaresUtils::GeoIP::RegionZonePair& startingTimezone )
{
QString region = CalamaresUtils::getString( configurationMap, "region" );
QString zone = CalamaresUtils::getString( configurationMap, "zone" );
if ( !region.isEmpty() && !zone.isEmpty() )
{
startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( region, zone );
}
else
{
startingTimezone
= CalamaresUtils::GeoIP::RegionZonePair( QStringLiteral( "America" ), QStringLiteral( "New_York" ) );
}
if ( CalamaresUtils::getBool( configurationMap, "useSystemTimezone", false ) )
{
auto systemtz = CalamaresUtils::GeoIP::splitTZString( QTimeZone::systemTimeZoneId() );
if ( systemtz.isValid() )
{
cDebug() << "Overriding configured timezone" << startingTimezone << "with system timezone" << systemtz;
startingTimezone = systemtz;
}
}
}
static inline void
getGeoIP( const QVariantMap& configurationMap, std::unique_ptr< CalamaresUtils::GeoIP::Handler >& geoip )
{
bool ok = false;
QVariantMap map = CalamaresUtils::getSubMap( configurationMap, "geoip", ok );
if ( ok )
{
QString url = CalamaresUtils::getString( map, "url" );
QString style = CalamaresUtils::getString( map, "style" );
QString selector = CalamaresUtils::getString( map, "selector" );
geoip = std::make_unique< CalamaresUtils::GeoIP::Handler >( style, url, selector );
if ( !geoip->isValid() )
{
cWarning() << "GeoIP Style" << style << "is not recognized.";
}
}
}
void
Config::setConfigurationMap( const QVariantMap& configurationMap )
{
getLocaleGenLines( configurationMap, m_localeGenLines );
getAdjustLiveTimezone( configurationMap, m_adjustLiveTimezone );
getStartingTimezone( configurationMap, m_startingTimezone );
getGeoIP( configurationMap, m_geoip );
if ( m_geoip && m_geoip->isValid() )
{
connect(
Calamares::ModuleManager::instance(), &Calamares::ModuleManager::modulesLoaded, this, &Config::startGeoIP );
}
}
Calamares::JobList
Config::createJobs()
{
Calamares::JobList list;
const CalamaresUtils::Locale::TZZone* location = currentLocation();
if ( location )
{
Calamares::Job* j = new SetTimezoneJob( location->region(), location->zone() );
list.append( Calamares::job_ptr( j ) );
}
return list;
}
void
Config::startGeoIP()
{
if ( m_geoip && m_geoip->isValid() )
{
auto& network = CalamaresUtils::Network::Manager::instance();
if ( network.hasInternet() || network.synchronousPing( m_geoip->url() ) )
{
using Watcher = QFutureWatcher< CalamaresUtils::GeoIP::RegionZonePair >;
m_geoipWatcher = std::make_unique< Watcher >();
m_geoipWatcher->setFuture( m_geoip->query() );
connect( m_geoipWatcher.get(), &Watcher::finished, this, &Config::completeGeoIP );
}
}
}
void
Config::completeGeoIP()
{
if ( !currentLocation() )
{
auto r = m_geoipWatcher->result();
if ( r.isValid() )
{
m_startingTimezone = r;
}
else
{
cWarning() << "GeoIP returned invalid result.";
}
}
else
{
cWarning() << "GeoIP result ignored because a location is already set.";
}
m_geoipWatcher.reset();
m_geoip.reset();
}

View File

@ -1,7 +1,8 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019-2020, Adriaan de Groot <groot@kde.org>
* Copyright 2020, Camilo Higuita <milo.h@aol.com>
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -23,9 +24,11 @@
#include "LocaleConfiguration.h"
#include "Job.h"
#include "geoip/Handler.h"
#include "geoip/Interface.h"
#include "locale/TimeZone.h"
#include <QAbstractListModel>
#include <QFutureWatcher>
#include <QObject>
#include <memory>
@ -33,53 +36,150 @@
class Config : public QObject
{
Q_OBJECT
Q_PROPERTY( const QStringList& supportedLocales READ supportedLocales CONSTANT FINAL )
Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* zonesModel READ zonesModel CONSTANT FINAL )
Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* regionModel READ regionModel CONSTANT FINAL )
Q_PROPERTY( const CalamaresUtils::Locale::TZZone* currentLocation READ currentLocation WRITE setCurrentLocation
NOTIFY currentLocationChanged )
// Status are complete, human-readable, messages
Q_PROPERTY( QString currentLocationStatus READ currentLocationStatus NOTIFY currentLanguageStatusChanged )
Q_PROPERTY( QString currentLanguageStatus READ currentLanguageStatus NOTIFY currentLanguageStatusChanged )
Q_PROPERTY( QString currentLCStatus READ currentLCStatus NOTIFY currentLCStatusChanged )
// Code are internal identifiers, like "en_US.UTF-8"
Q_PROPERTY( QString currentLanguageCode READ currentLanguageCode WRITE setLanguageExplicitly NOTIFY currentLanguageCodeChanged )
Q_PROPERTY( QString currentLCCode READ currentLCCode WRITE setLCLocaleExplicitly NOTIFY currentLCCodeChanged )
// This is a long human-readable string with all three statuses
Q_PROPERTY( QString prettyStatus READ prettyStatus NOTIFY prettyStatusChanged FINAL )
public:
Config( QObject* parent = nullptr );
~Config();
CalamaresUtils::Locale::CStringListModel* regionModel() const;
CalamaresUtils::Locale::CStringListModel* zonesModel() const;
void setLocaleInfo( const QString& initialRegion, const QString& initialZone, const QString& localeGenPath );
void setConfigurationMap( const QVariantMap& );
Calamares::JobList createJobs();
QMap< QString, QString > localesMap();
// Underlying data for the models
const CalamaresUtils::Locale::CStringPairList& timezoneData() const;
/** @brief The currently selected location (timezone)
*
* The location is a pointer into the date that timezoneData() returns.
*/
const CalamaresUtils::Locale::TZZone* currentLocation() const { return m_currentLocation; }
/// locale configuration (LC_* and LANG) based solely on the current location.
LocaleConfiguration automaticLocaleConfiguration() const;
/// locale configuration that takes explicit settings into account
LocaleConfiguration localeConfiguration() const;
/// The human-readable description of what timezone is used
QString currentLocationStatus() const;
/// The human-readable description of what language is used
QString currentLanguageStatus() const;
/// The human-readable description of what locale (LC_*) is used
QString currentLCStatus() const;
/// The human-readable summary of what the module will do
QString prettyStatus() const;
private:
CalamaresUtils::Locale::CStringPairList m_regionList;
CalamaresUtils::Locale::CStringListModel* m_regionModel;
CalamaresUtils::Locale::CStringListModel* m_zonesModel;
const QStringList& supportedLocales() const { return m_localeGenLines; }
CalamaresUtils::Locale::CStringListModel* regionModel() const { return m_regionModel.get(); }
CalamaresUtils::Locale::CStringListModel* zonesModel() const { return m_zonesModel.get(); }
LocaleConfiguration m_selectedLocaleConfiguration;
/// Special case, set location from starting timezone if not already set
void setCurrentLocation();
QStringList m_localeGenLines;
int m_currentRegion = -1;
public Q_SLOTS:
/// Set a language by user-choice, overriding future location changes
void setLanguageExplicitly( const QString& language );
/// Set LC (formats) by user-choice, overriding future location changes
void setLCLocaleExplicitly( const QString& locale );
bool m_blockTzWidgetSet;
LocaleConfiguration guessLocaleConfiguration() const;
// For the given locale config, return two strings describing
// the settings for language and numbers.
std::pair< QString, QString > prettyLocaleStatus( const LocaleConfiguration& ) const;
/** @brief Update the GS *locale* key with the selected system language.
/** @brief Sets a location by full name
*
* This uses whatever is set in m_selectedLocaleConfiguration as the language,
* and writes it to GS *locale* key (as a string, in BCP47 format).
* @p regionzone should be an identifier from zone.tab, e.g. "Africa/Abidjan",
* which is split into regon and zone. Invalid names will **not**
* change the actual location.
*/
void updateGlobalLocale();
void updateGlobalStorage();
void updateLocaleLabels();
void setCurrentLocation( const QString& regionzone );
/** @brief Sets a location by split name
*
* @p region should be "America" or the like, while @p zone
* names a zone within that region.
*/
void setCurrentLocation( const QString& region, const QString& zone );
/** @brief Sets a location by pointer
*
* Pointer should be within the same model as the widget uses.
* This can update the locale configuration -- the automatic one
* follows the current location, and otherwise only explicitly-set
* values will ignore changes to the location.
*/
void setCurrentLocation( const CalamaresUtils::Locale::TZZone* location );
const CalamaresUtils::Locale::TZZone* currentLocation() const;
QString currentLanguageCode() const { return localeConfiguration().language(); }
QString currentLCCode() const { return localeConfiguration().lc_numeric; }
signals:
void prettyStatusChanged();
void currentLocationChanged( const CalamaresUtils::Locale::TZZone* location ) const;
void currentLocationStatusChanged( const QString& ) const;
void currentLanguageStatusChanged( const QString& ) const;
void currentLCStatusChanged( const QString& ) const;
void prettyStatusChanged( const QString& ) const;
void currentLanguageCodeChanged( const QString& ) const;
void currentLCCodeChanged( const QString& ) const;
private:
/// A list of supported locale identifiers (e.g. "en_US.UTF-8")
QStringList m_localeGenLines;
/// The regions (America, Asia, Europe ..)
std::unique_ptr< CalamaresUtils::Locale::CStringListModel > m_regionModel;
/// The zones for the current region (e.g. America/New_York)
std::unique_ptr< CalamaresUtils::Locale::CStringListModel > m_zonesModel;
/// The location, points into the timezone data
const CalamaresUtils::Locale::TZZone* m_currentLocation = nullptr;
/** @brief Specific locale configurations
*
* "Automatic" locale configuration based on the location (e.g.
* Europe/Amsterdam means Dutch language and Dutch locale) leave
* this empty; if the user explicitly sets something, then
* this configuration is non-empty and takes precedence over the
* automatic location settings (so a user in Amsterdam can still
* pick Ukranian settings, for instance).
*/
LocaleConfiguration m_selectedLocaleConfiguration;
/** @brief Should we adjust the "live" timezone when the location changes?
*
* In the Widgets UI, clicking around on the world map adjusts the
* timezone, and the live system can be made to follow that.
*/
bool m_adjustLiveTimezone;
/** @brief The initial timezone (region, zone) specified in the config.
*
* This may be overridden by setting *useSystemTimezone* or by
* GeoIP settings.
*/
CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone;
/** @brief Handler for GeoIP lookup (if configured)
*
* The GeoIP lookup needs to be started at some suitable time,
* by explicitly calling *TODO*
*/
std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip;
// Implementation details for doing GeoIP lookup
void startGeoIP();
void completeGeoIP();
std::unique_ptr< QFutureWatcher< CalamaresUtils::GeoIP::RegionZonePair > > m_geoipWatcher;
};

View File

@ -37,7 +37,7 @@ LocaleConfiguration::LocaleConfiguration( const QString& localeName, const QStri
lc_numeric = lc_time = lc_monetary = lc_paper = lc_name = lc_address = lc_telephone = lc_measurement
= lc_identification = formatsName;
(void)setLanguage( localeName );
setLanguage( localeName );
}
@ -83,7 +83,7 @@ LocaleConfiguration::fromLanguageAndLocation( const QString& languageLocale,
if ( language == "pt" || language == "zh" )
{
QString proposedLocale = QString( "%1_%2" ).arg( language ).arg( countryCode );
foreach ( QString line, linesForLanguage )
for ( const QString& line : linesForLanguage )
{
if ( line.contains( proposedLocale ) )
{

View File

@ -26,36 +26,53 @@
class LocaleConfiguration
{
public:
/// @brief Create an empty locale, with nothing set
explicit LocaleConfiguration();
/// @brief Create a locale with everything set to the given @p localeName
public: // TODO: private (but need to be public for tests)
/** @brief Create a locale with everything set to the given @p localeName
*
* Consumers should use fromLanguageAndLocation() instead.
*/
explicit LocaleConfiguration( const QString& localeName /* "en_US.UTF-8" */ )
: LocaleConfiguration( localeName, localeName )
{
}
/// @brief Create a locale with language and formats separate
/** @brief Create a locale with language and formats separate
*
* Consumers should use fromLanguageAndLocation() instead.
*/
explicit LocaleConfiguration( const QString& localeName, const QString& formatsName );
/// @brief Create an empty locale, with nothing set
explicit LocaleConfiguration();
/** @brief Create a "sensible" locale configuration for @p language and @p countryCode
*
* This method applies some heuristics to pick a good locale (from the list
* @p availableLocales), along with a good language (for instance, in
* large countries with many languages, picking a generally used one).
*/
static LocaleConfiguration
fromLanguageAndLocation( const QString& language, const QStringList& availableLocales, const QString& countryCode );
/// Is this an empty (default-constructed and not modified) configuration?
bool isEmpty() const;
/** @brief sets lang and the BCP47 representation
/** @brief sets language to @p localeName
*
* Note that the documentation how this works is in packages.conf
* The language may be regionalized, e.g. "nl_BE". Both the language
* (with region) and BCP47 representation (without region, lowercase)
* are updated. The BCP47 representation is used by the packages module.
* See also `packages.conf` for a discussion of how this is used.
*/
void setLanguage( const QString& localeName );
/// Current language (including region)
QString language() const { return m_lang; }
// Note that the documentation how this works is in packages.conf
/// Current language (lowercase, BCP47 format, no region)
QString toBcp47() const { return m_languageLocaleBcp47; }
QMap< QString, QString > toMap() const;
// These become all uppercase in locale.conf, but we keep them lowercase here to
// avoid confusion with locale.h.
// avoid confusion with <locale.h>, which defines (e.g.) LC_NUMERIC macro.
QString lc_numeric, lc_time, lc_monetary, lc_paper, lc_name, lc_address, lc_telephone, lc_measurement,
lc_identification;

View File

@ -19,37 +19,30 @@
#include "LocalePage.h"
#include "SetTimezoneJob.h"
#include "Config.h"
#include "LCLocaleDialog.h"
#include "timezonewidget/timezonewidget.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "LCLocaleDialog.h"
#include "Settings.h"
#include "locale/Label.h"
#include "locale/TimeZone.h"
#include "utils/CalamaresUtilsGui.h"
#include "utils/Logger.h"
#include "utils/RAII.h"
#include "utils/Retranslator.h"
#include <QBoxLayout>
#include <QComboBox>
#include <QFile>
#include <QLabel>
#include <QProcess>
#include <QPointer>
#include <QPushButton>
LocalePage::LocalePage( QWidget* parent )
LocalePage::LocalePage( Config* config, QWidget* parent )
: QWidget( parent )
, m_regionList( CalamaresUtils::Locale::TZRegion::fromZoneTab() )
, m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( m_regionList ) )
, m_config( config )
, m_blockTzWidgetSet( false )
{
QBoxLayout* mainLayout = new QVBoxLayout;
QBoxLayout* tzwLayout = new QHBoxLayout;
m_tzWidget = new TimeZoneWidget( this );
m_tzWidget = new TimeZoneWidget( config->timezoneData(), this );
tzwLayout->addStretch();
tzwLayout->addWidget( m_tzWidget );
tzwLayout->addStretch();
@ -105,9 +98,24 @@ LocalePage::LocalePage( QWidget* parent )
setMinimumWidth( m_tzWidget->width() );
setLayout( mainLayout );
// Set up the location before connecting signals, to avoid a signal
// storm as various parts interact.
m_regionCombo->setModel( m_config->regionModel() );
locationChanged( m_config->currentLocation() ); // doesn't inform TZ widget
m_tzWidget->setCurrentLocation( m_config->currentLocation() );
connect( config, &Config::currentLCStatusChanged, m_formatsLabel, &QLabel::setText );
connect( config, &Config::currentLanguageStatusChanged, m_localeLabel, &QLabel::setText );
connect( config, &Config::currentLocationChanged, m_tzWidget, &TimeZoneWidget::setCurrentLocation );
connect( config, &Config::currentLocationChanged, this, &LocalePage::locationChanged );
connect( m_tzWidget,
&TimeZoneWidget::locationChanged,
config,
QOverload< const CalamaresUtils::Locale::TZZone* >::of( &Config::setCurrentLocation ) );
connect( m_regionCombo, QOverload< int >::of( &QComboBox::currentIndexChanged ), this, &LocalePage::regionChanged );
connect( m_zoneCombo, QOverload< int >::of( &QComboBox::currentIndexChanged ), this, &LocalePage::zoneChanged );
connect( m_tzWidget, &TimeZoneWidget::locationChanged, this, &LocalePage::locationChanged );
connect( m_localeChangeButton, &QPushButton::clicked, this, &LocalePage::changeLocale );
connect( m_formatsChangeButton, &QPushButton::clicked, this, &LocalePage::changeFormats );
@ -115,10 +123,7 @@ LocalePage::LocalePage( QWidget* parent )
}
LocalePage::~LocalePage()
{
qDeleteAll( m_regionList );
}
LocalePage::~LocalePage() {}
void
@ -128,175 +133,8 @@ LocalePage::updateLocaleLabels()
m_zoneLabel->setText( tr( "Zone:" ) );
m_localeChangeButton->setText( tr( "&Change..." ) );
m_formatsChangeButton->setText( tr( "&Change..." ) );
LocaleConfiguration lc
= m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration() : m_selectedLocaleConfiguration;
auto labels = prettyLocaleStatus( lc );
m_localeLabel->setText( labels.first );
m_formatsLabel->setText( labels.second );
}
void
LocalePage::init( const QString& initialRegion, const QString& initialZone, const QString& localeGenPath )
{
using namespace CalamaresUtils::Locale;
m_regionCombo->setModel( m_regionModel.get() );
m_regionCombo->currentIndexChanged( m_regionCombo->currentIndex() );
auto* region = m_regionList.find< TZRegion >( initialRegion );
if ( region && region->zones().find< TZZone >( initialZone ) )
{
m_tzWidget->setCurrentLocation( initialRegion, initialZone );
}
else
{
m_tzWidget->setCurrentLocation( "America", "New_York" );
}
// Some distros come with a meaningfully commented and easy to parse locale.gen,
// and others ship a separate file /usr/share/i18n/SUPPORTED with a clean list of
// supported locales. We first try that one, and if it doesn't exist, we fall back
// to parsing the lines from locale.gen
m_localeGenLines.clear();
QFile supported( "/usr/share/i18n/SUPPORTED" );
QByteArray ba;
if ( supported.exists() && supported.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
ba = supported.readAll();
supported.close();
const auto lines = ba.split( '\n' );
for ( const QByteArray& line : lines )
{
m_localeGenLines.append( QString::fromLatin1( line.simplified() ) );
}
}
else
{
QFile localeGen( localeGenPath );
if ( localeGen.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
ba = localeGen.readAll();
localeGen.close();
}
else
{
cWarning() << "Cannot open file" << localeGenPath
<< ". Assuming the supported languages are already built into "
"the locale archive.";
QProcess localeA;
localeA.start( "locale", QStringList() << "-a" );
localeA.waitForFinished();
ba = localeA.readAllStandardOutput();
}
const auto lines = ba.split( '\n' );
for ( const QByteArray& line : lines )
{
if ( line.startsWith( "## " ) || line.startsWith( "# " ) || line.simplified() == "#" )
{
continue;
}
QString lineString = QString::fromLatin1( line.simplified() );
if ( lineString.startsWith( "#" ) )
{
lineString.remove( '#' );
}
lineString = lineString.simplified();
if ( lineString.isEmpty() )
{
continue;
}
m_localeGenLines.append( lineString );
}
}
if ( m_localeGenLines.isEmpty() )
{
cWarning() << "cannot acquire a list of available locales."
<< "The locale and localecfg modules will be broken as long as this "
"system does not provide"
<< "\n\t "
<< "* a well-formed" << supported.fileName() << "\n\tOR"
<< "* a well-formed"
<< ( localeGenPath.isEmpty() ? QLatin1String( "/etc/locale.gen" ) : localeGenPath ) << "\n\tOR"
<< "* a complete pre-compiled locale-gen database which allows complete locale -a output.";
return; // something went wrong and there's nothing we can do about it.
}
// Assuming we have a list of supported locales, we usually only want UTF-8 ones
// because it's not 1995.
auto notUtf8 = []( const QString& s ) {
return !s.contains( "UTF-8", Qt::CaseInsensitive ) && !s.contains( "utf8", Qt::CaseInsensitive );
};
auto it = std::remove_if( m_localeGenLines.begin(), m_localeGenLines.end(), notUtf8 );
m_localeGenLines.erase( it, m_localeGenLines.end() );
// We strip " UTF-8" from "en_US.UTF-8 UTF-8" because it's redundant redundant.
// Also simplify whitespace.
auto unredundant = []( QString& s ) {
if ( s.endsWith( " UTF-8" ) )
{
s.chop( 6 );
}
s = s.simplified();
};
std::for_each( m_localeGenLines.begin(), m_localeGenLines.end(), unredundant );
updateGlobalStorage();
}
std::pair< QString, QString >
LocalePage::prettyLocaleStatus( const LocaleConfiguration& lc ) const
{
using CalamaresUtils::Locale::Label;
Label lang( lc.language(), Label::LabelFormat::AlwaysWithCountry );
Label num( lc.lc_numeric, Label::LabelFormat::AlwaysWithCountry );
return std::make_pair< QString, QString >(
tr( "The system language will be set to %1." ).arg( lang.label() ),
tr( "The numbers and dates locale will be set to %1." ).arg( num.label() ) );
}
QString
LocalePage::prettyStatus() const
{
QString status;
status += tr( "Set timezone to %1/%2.<br/>" ).arg( m_regionCombo->currentText() ).arg( m_zoneCombo->currentText() );
LocaleConfiguration lc
= m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration() : m_selectedLocaleConfiguration;
auto labels = prettyLocaleStatus( lc );
status += labels.first + "<br/>";
status += labels.second + "<br/>";
return status;
}
Calamares::JobList
LocalePage::createJobs()
{
QList< Calamares::job_ptr > list;
const CalamaresUtils::Locale::TZZone* location = m_tzWidget->currentLocation();
Calamares::Job* j = new SetTimezoneJob( location->region(), location->zone() );
list.append( Calamares::job_ptr( j ) );
return list;
}
QMap< QString, QString >
LocalePage::localesMap()
{
return m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().toMap()
: m_selectedLocaleConfiguration.toMap();
m_localeLabel->setText( m_config->currentLanguageStatus() );
m_formatsLabel->setText( m_config->currentLCStatus() );
}
@ -304,82 +142,10 @@ void
LocalePage::onActivate()
{
m_regionCombo->setFocus();
if ( m_selectedLocaleConfiguration.isEmpty() || !m_selectedLocaleConfiguration.explicit_lang )
{
auto newLocale = guessLocaleConfiguration();
m_selectedLocaleConfiguration.setLanguage( newLocale.language() );
updateGlobalLocale();
updateLocaleLabels();
}
}
LocaleConfiguration
LocalePage::guessLocaleConfiguration() const
{
return LocaleConfiguration::fromLanguageAndLocation(
QLocale().name(), m_localeGenLines, m_tzWidget->currentLocation()->country() );
}
void
LocalePage::updateGlobalLocale()
{
auto* gs = Calamares::JobQueue::instance()->globalStorage();
const QString bcp47 = m_selectedLocaleConfiguration.toBcp47();
gs->insert( "locale", bcp47 );
}
void
LocalePage::updateGlobalStorage()
{
auto* gs = Calamares::JobQueue::instance()->globalStorage();
const auto* location = m_tzWidget->currentLocation();
bool locationChanged = ( location->region() != gs->value( "locationRegion" ) )
|| ( location->zone() != gs->value( "locationZone" ) );
gs->insert( "locationRegion", location->region() );
gs->insert( "locationZone", location->zone() );
updateGlobalLocale();
// If we're in chroot mode (normal install mode), then we immediately set the
// timezone on the live system. When debugging timezones, don't bother.
#ifndef DEBUG_TIMEZONES
if ( locationChanged && Calamares::Settings::instance()->doChroot() )
{
QProcess::execute( "timedatectl", // depends on systemd
{ "set-timezone", location->region() + '/' + location->zone() } );
}
#endif
// Preserve those settings that have been made explicit.
auto newLocale = guessLocaleConfiguration();
if ( !m_selectedLocaleConfiguration.isEmpty() && m_selectedLocaleConfiguration.explicit_lang )
{
newLocale.setLanguage( m_selectedLocaleConfiguration.language() );
}
if ( !m_selectedLocaleConfiguration.isEmpty() && m_selectedLocaleConfiguration.explicit_lc )
{
newLocale.lc_numeric = m_selectedLocaleConfiguration.lc_numeric;
newLocale.lc_time = m_selectedLocaleConfiguration.lc_time;
newLocale.lc_monetary = m_selectedLocaleConfiguration.lc_monetary;
newLocale.lc_paper = m_selectedLocaleConfiguration.lc_paper;
newLocale.lc_name = m_selectedLocaleConfiguration.lc_name;
newLocale.lc_address = m_selectedLocaleConfiguration.lc_address;
newLocale.lc_telephone = m_selectedLocaleConfiguration.lc_telephone;
newLocale.lc_measurement = m_selectedLocaleConfiguration.lc_measurement;
newLocale.lc_identification = m_selectedLocaleConfiguration.lc_identification;
}
newLocale.explicit_lang = m_selectedLocaleConfiguration.explicit_lang;
newLocale.explicit_lc = m_selectedLocaleConfiguration.explicit_lc;
m_selectedLocaleConfiguration = newLocale;
updateLocaleLabels();
}
void
LocalePage::regionChanged( int currentIndex )
{
@ -388,15 +154,17 @@ LocalePage::regionChanged( int currentIndex )
Q_UNUSED( currentIndex )
QString selectedRegion = m_regionCombo->currentData().toString();
TZRegion* region = m_regionList.find< TZRegion >( selectedRegion );
TZRegion* region = m_config->timezoneData().find< TZRegion >( selectedRegion );
if ( !region )
{
return;
}
m_zoneCombo->blockSignals( true );
m_zoneCombo->setModel( new CStringListModel( region->zones() ) );
m_zoneCombo->blockSignals( false );
{
cSignalBlocker b( m_zoneCombo );
m_zoneCombo->setModel( new CStringListModel( region->zones() ) );
}
m_zoneCombo->currentIndexChanged( m_zoneCombo->currentIndex() );
}
@ -405,16 +173,19 @@ LocalePage::zoneChanged( int currentIndex )
{
Q_UNUSED( currentIndex )
if ( !m_blockTzWidgetSet )
m_tzWidget->setCurrentLocation( m_regionCombo->currentData().toString(),
m_zoneCombo->currentData().toString() );
updateGlobalStorage();
{
m_config->setCurrentLocation( m_regionCombo->currentData().toString(), m_zoneCombo->currentData().toString() );
}
}
void
LocalePage::locationChanged( const CalamaresUtils::Locale::TZZone* location )
{
m_blockTzWidgetSet = true;
if ( !location )
{
return;
}
cBoolSetter< true > b( m_blockTzWidgetSet );
// Set region index
int index = m_regionCombo->findData( location->region() );
@ -433,58 +204,35 @@ LocalePage::locationChanged( const CalamaresUtils::Locale::TZZone* location )
}
m_zoneCombo->setCurrentIndex( index );
m_blockTzWidgetSet = false;
updateGlobalStorage();
}
void
LocalePage::changeLocale()
{
LCLocaleDialog* dlg
= new LCLocaleDialog( m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().language()
: m_selectedLocaleConfiguration.language(),
m_localeGenLines,
this );
QPointer< LCLocaleDialog > dlg(
new LCLocaleDialog( m_config->localeConfiguration().language(), m_config->supportedLocales(), this ) );
dlg->exec();
if ( dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() )
if ( dlg && dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() )
{
m_selectedLocaleConfiguration.setLanguage( dlg->selectedLCLocale() );
m_selectedLocaleConfiguration.explicit_lang = true;
this->updateGlobalLocale();
this->updateLocaleLabels();
m_config->setLanguageExplicitly( dlg->selectedLCLocale() );
updateLocaleLabels();
}
dlg->deleteLater();
delete dlg;
}
void
LocalePage::changeFormats()
{
LCLocaleDialog* dlg
= new LCLocaleDialog( m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().lc_numeric
: m_selectedLocaleConfiguration.lc_numeric,
m_localeGenLines,
this );
QPointer< LCLocaleDialog > dlg(
new LCLocaleDialog( m_config->localeConfiguration().lc_numeric, m_config->supportedLocales(), this ) );
dlg->exec();
if ( dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() )
if ( dlg && dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() )
{
// TODO: improve the granularity of this setting.
m_selectedLocaleConfiguration.lc_numeric = dlg->selectedLCLocale();
m_selectedLocaleConfiguration.lc_time = dlg->selectedLCLocale();
m_selectedLocaleConfiguration.lc_monetary = dlg->selectedLCLocale();
m_selectedLocaleConfiguration.lc_paper = dlg->selectedLCLocale();
m_selectedLocaleConfiguration.lc_name = dlg->selectedLCLocale();
m_selectedLocaleConfiguration.lc_address = dlg->selectedLCLocale();
m_selectedLocaleConfiguration.lc_telephone = dlg->selectedLCLocale();
m_selectedLocaleConfiguration.lc_measurement = dlg->selectedLCLocale();
m_selectedLocaleConfiguration.lc_identification = dlg->selectedLCLocale();
m_selectedLocaleConfiguration.explicit_lc = true;
this->updateLocaleLabels();
m_config->setLCLocaleExplicitly( dlg->selectedLCLocale() );
updateLocaleLabels();
}
dlg->deleteLater();
delete dlg;
}

View File

@ -32,39 +32,23 @@
class QComboBox;
class QLabel;
class QPushButton;
class Config;
class TimeZoneWidget;
class LocalePage : public QWidget
{
Q_OBJECT
public:
explicit LocalePage( QWidget* parent = nullptr );
explicit LocalePage( class Config* config, QWidget* parent = nullptr );
virtual ~LocalePage();
void init( const QString& initialRegion, const QString& initialZone, const QString& localeGenPath );
QString prettyStatus() const;
Calamares::JobList createJobs();
QMap< QString, QString > localesMap();
void onActivate();
private:
LocaleConfiguration guessLocaleConfiguration() const;
/// @brief Non-owning pointer to the ViewStep's config
Config* m_config;
// For the given locale config, return two strings describing
// the settings for language and numbers.
std::pair< QString, QString > prettyLocaleStatus( const LocaleConfiguration& ) const;
/** @brief Update the GS *locale* key with the selected system language.
*
* This uses whatever is set in m_selectedLocaleConfiguration as the language,
* and writes it to GS *locale* key (as a string, in BCP47 format).
*/
void updateGlobalLocale();
void updateGlobalStorage();
void updateLocaleLabels();
void regionChanged( int currentIndex );
@ -73,10 +57,6 @@ private:
void changeLocale();
void changeFormats();
// Dynamically, QList< TZRegion* >
CalamaresUtils::Locale::CStringPairList m_regionList;
std::unique_ptr< CalamaresUtils::Locale::CStringListModel > m_regionModel;
TimeZoneWidget* m_tzWidget;
QComboBox* m_regionCombo;
QComboBox* m_zoneCombo;
@ -88,9 +68,6 @@ private:
QLabel* m_formatsLabel;
QPushButton* m_formatsChangeButton;
LocaleConfiguration m_selectedLocaleConfiguration;
QStringList m_localeGenLines;
bool m_blockTzWidgetSet;
};

View File

@ -34,7 +34,6 @@
#include <QBoxLayout>
#include <QLabel>
#include <QtConcurrent/QtConcurrentRun>
CALAMARES_PLUGIN_FACTORY_DEFINITION( LocaleViewStepFactory, registerPlugin< LocaleViewStep >(); )
@ -44,7 +43,7 @@ LocaleViewStep::LocaleViewStep( QObject* parent )
, m_widget( new QWidget() )
, m_actualWidget( nullptr )
, m_nextEnabled( false )
, m_geoip( nullptr )
, m_config( std::make_unique< Config >() )
{
QBoxLayout* mainLayout = new QHBoxLayout;
m_widget->setLayout( mainLayout );
@ -66,11 +65,11 @@ LocaleViewStep::~LocaleViewStep()
void
LocaleViewStep::setUpPage()
{
m_config->setCurrentLocation();
if ( !m_actualWidget )
{
m_actualWidget = new LocalePage();
m_actualWidget = new LocalePage( m_config.get() );
}
m_actualWidget->init( m_startingTimezone.first, m_startingTimezone.second, m_localeGenPath );
m_widget->layout()->addWidget( m_actualWidget );
ensureSize( m_actualWidget->sizeHint() );
@ -80,20 +79,6 @@ LocaleViewStep::setUpPage()
}
void
LocaleViewStep::fetchGeoIpTimezone()
{
if ( m_geoip && m_geoip->isValid() )
{
m_startingTimezone = m_geoip->get();
if ( !m_startingTimezone.isValid() )
{
cWarning() << "GeoIP lookup at" << m_geoip->url() << "failed.";
}
}
}
QString
LocaleViewStep::prettyName() const
{
@ -104,7 +89,7 @@ LocaleViewStep::prettyName() const
QString
LocaleViewStep::prettyStatus() const
{
return m_prettyStatus;
return m_config->prettyStatus();
}
@ -146,7 +131,7 @@ LocaleViewStep::isAtEnd() const
Calamares::JobList
LocaleViewStep::jobs() const
{
return m_jobs;
return m_config->createJobs();
}
@ -164,83 +149,11 @@ LocaleViewStep::onActivate()
void
LocaleViewStep::onLeave()
{
if ( m_actualWidget )
{
m_jobs = m_actualWidget->createJobs();
m_prettyStatus = m_actualWidget->prettyStatus();
auto map = m_actualWidget->localesMap();
QVariantMap vm;
for ( auto it = map.constBegin(); it != map.constEnd(); ++it )
{
vm.insert( it.key(), it.value() );
}
Calamares::JobQueue::instance()->globalStorage()->insert( "localeConf", vm );
}
else
{
m_jobs.clear();
Calamares::JobQueue::instance()->globalStorage()->remove( "localeConf" );
}
}
void
LocaleViewStep::setConfigurationMap( const QVariantMap& configurationMap )
{
QString region = CalamaresUtils::getString( configurationMap, "region" );
QString zone = CalamaresUtils::getString( configurationMap, "zone" );
if ( !region.isEmpty() && !zone.isEmpty() )
{
m_startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( region, zone );
}
else
{
m_startingTimezone
= CalamaresUtils::GeoIP::RegionZonePair( QStringLiteral( "America" ), QStringLiteral( "New_York" ) );
}
m_localeGenPath = CalamaresUtils::getString( configurationMap, "localeGenPath" );
if ( m_localeGenPath.isEmpty() )
{
m_localeGenPath = QStringLiteral( "/etc/locale.gen" );
}
bool ok = false;
QVariantMap geoip = CalamaresUtils::getSubMap( configurationMap, "geoip", ok );
if ( ok )
{
QString url = CalamaresUtils::getString( geoip, "url" );
QString style = CalamaresUtils::getString( geoip, "style" );
QString selector = CalamaresUtils::getString( geoip, "selector" );
m_geoip = std::make_unique< CalamaresUtils::GeoIP::Handler >( style, url, selector );
if ( !m_geoip->isValid() )
{
cWarning() << "GeoIP Style" << style << "is not recognized.";
}
}
}
Calamares::RequirementsList
LocaleViewStep::checkRequirements()
{
if ( m_geoip && m_geoip->isValid() )
{
auto& network = CalamaresUtils::Network::Manager::instance();
if ( network.hasInternet() )
{
fetchGeoIpTimezone();
}
else
{
if ( network.synchronousPing( m_geoip->url() ) )
{
fetchGeoIpTimezone();
}
}
}
return Calamares::RequirementsList();
m_config->setConfigurationMap( configurationMap );
}

View File

@ -20,15 +20,11 @@
#ifndef LOCALEVIEWSTEP_H
#define LOCALEVIEWSTEP_H
#include "geoip/Handler.h"
#include "geoip/Interface.h"
#include "utils/PluginFactory.h"
#include "viewpages/ViewStep.h"
#include "Config.h"
#include "DllMacro.h"
#include <QFutureWatcher>
#include <QObject>
#include "utils/PluginFactory.h"
#include "viewpages/ViewStep.h"
#include <memory>
@ -60,25 +56,16 @@ public:
void setConfigurationMap( const QVariantMap& configurationMap ) override;
/// @brief Do setup (returns empty list) asynchronously
virtual Calamares::RequirementsList checkRequirements() override;
private slots:
void setUpPage();
private:
void fetchGeoIpTimezone();
QWidget* m_widget;
LocalePage* m_actualWidget;
bool m_nextEnabled;
QString m_prettyStatus;
CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone;
QString m_localeGenPath;
Calamares::JobList m_jobs;
std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip;
std::unique_ptr< Config > m_config;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( LocaleViewStepFactory )

View File

@ -1,5 +1,5 @@
---
# This settings are used to set your default system time zone.
# These settings are used to set your default system time zone.
# Time zones are usually located under /usr/share/zoneinfo and
# provided by the 'tzdata' package of your Distribution.
#
@ -11,20 +11,41 @@
# the locale page can be set through keys *region* and *zone*.
# If either is not set, defaults to America/New_York.
#
# Note that useSystemTimezone and GeoIP settings can change the
# starting time zone.
#
region: "America"
zone: "New_York"
# Instead of using *region* and *zone* specified above,
# you can use the system's notion of the timezone, instead.
# This can help if your system is automatically configured with
# a sensible TZ rather than chasing a fixed default.
#
# The default is false.
#
# useSystemTimezone: true
# Should changing the system location (e.g. clicking around on the timezone
# map) immediately reflect the changed timezone in the live system?
# By default, installers (with a target system) do, and setup (e.g. OEM
# configuration) does not, but you can switch it on here (or off, if
# you think it's annoying in the installer).
#
# Note that not all systems support live adjustment.
#
# adjustLiveTimezone: true
# System locales are detected in the following order:
#
# - /usr/share/i18n/SUPPORTED
# - localeGenPath (defaults to /etc/locale.gen if not set)
# - 'locale -a' output
# - `locale -a` output
#
# Enable only when your Distribution is using an
# Enable only when your Distribution is using a
# custom path for locale.gen
#
#localeGenPath: "PATH_TO/locale.gen"
#localeGenPath: "/etc/locale.gen"
# GeoIP based Language settings: Leave commented out to disable GeoIP.
#

View File

@ -4,7 +4,35 @@ $id: https://calamares.io/schemas/locale
additionalProperties: false
type: object
properties:
"region": { type: str }
"zone": { type: str }
"localeGenPath": { type: string, required: true }
"geoipUrl": { type: str }
region: { type: string,
enum: [
Africa,
America,
Antarctica,
Arctic,
Asia,
Atlantic,
Australia,
Europe,
Indian,
Pacific
]
}
zone: { type: string }
useSystemTimezone: { type: boolean, default: false }
adjustLiveTimezone: { type: boolean, default: true }
localeGenPath: { type: string }
# TODO: refactor, this is reused in welcome
geoip:
additionalProperties: false
type: object
properties:
style: { type: string, enum: [ none, fixed, xml, json ] }
url: { type: string }
selector: { type: string }
required: [ style, url, selector ]
required: [ region, zone ]

View File

@ -25,9 +25,9 @@
#include <cmath>
static const char* zoneNames[]
= { "0.0", "1.0", "2.0", "3.0", "3.5", "4.0", "4.5", "5.0", "5.5", "5.75", "6.0", "6.5", "7.0",
"8.0", "9.0", "9.5", "10.0", "10.5", "11.0", "12.0", "12.75", "13.0", "-1.0", "-2.0", "-3.0",
"-3.5", "-4.0", "-4.5", "-5.0", "-5.5", "-6.0", "-7.0", "-8.0", "-9.0", "-9.5", "-10.0", "-11.0" };
= { "0.0", "1.0", "2.0", "3.0", "3.5", "4.0", "4.5", "5.0", "5.5", "5.75", "6.0", "6.5", "7.0",
"8.0", "9.0", "9.5", "10.0", "10.5", "11.0", "12.0", "12.75", "13.0", "-1.0", "-2.0", "-3.0", "-3.5",
"-4.0", "-4.5", "-5.0", "-5.5", "-6.0", "-7.0", "-8.0", "-9.0", "-9.5", "-10.0", "-11.0" };
static_assert( TimeZoneImageList::zoneCount == ( sizeof( zoneNames ) / sizeof( zoneNames[ 0 ] ) ),
"Incorrect number of zones" );

View File

@ -34,9 +34,17 @@
#define ZONE_NAME QStringLiteral( "zone" )
#endif
TimeZoneWidget::TimeZoneWidget( QWidget* parent )
static QPoint
getLocationPosition( const CalamaresUtils::Locale::TZZone* l )
{
return TimeZoneImageList::getLocationPosition( l->longitude(), l->latitude() );
}
TimeZoneWidget::TimeZoneWidget( const CalamaresUtils::Locale::CStringPairList& zones, QWidget* parent )
: QWidget( parent )
, timeZoneImages( TimeZoneImageList::fromQRC() )
, m_zonesData( zones )
{
setMouseTracking( false );
setCursor( Qt::PointingHandCursor );
@ -57,27 +65,13 @@ TimeZoneWidget::TimeZoneWidget( QWidget* parent )
void
TimeZoneWidget::setCurrentLocation( QString regionName, QString zoneName )
TimeZoneWidget::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location )
{
using namespace CalamaresUtils::Locale;
const auto& regions = TZRegion::fromZoneTab();
auto* region = regions.find< TZRegion >( regionName );
if ( !region )
if ( location == m_currentLocation )
{
return;
}
auto* zone = region->zones().find< TZZone >( zoneName );
if ( zone )
{
setCurrentLocation( zone );
}
}
void
TimeZoneWidget::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location )
{
m_currentLocation = location;
// Set zone
@ -93,7 +87,6 @@ TimeZoneWidget::setCurrentLocation( const CalamaresUtils::Locale::TZZone* locati
// Repaint widget
repaint();
emit locationChanged( m_currentLocation );
}
@ -101,11 +94,18 @@ TimeZoneWidget::setCurrentLocation( const CalamaresUtils::Locale::TZZone* locati
//### Private
//###
struct PainterEnder
{
QPainter& p;
~PainterEnder() { p.end(); }
};
void
TimeZoneWidget::paintEvent( QPaintEvent* )
{
QFontMetrics fontMetrics( font );
QPainter painter( this );
PainterEnder painter_end { painter };
painter.setRenderHint( QPainter::Antialiasing );
painter.setFont( font );
@ -116,6 +116,11 @@ TimeZoneWidget::paintEvent( QPaintEvent* )
// Draw zone image
painter.drawImage( 0, 0, currentZoneImage );
if ( !m_currentLocation )
{
return;
}
#ifdef DEBUG_TIMEZONES
QPoint point = getLocationPosition( m_currentLocation );
// Draw latitude lines
@ -175,8 +180,6 @@ TimeZoneWidget::paintEvent( QPaintEvent* )
painter.setPen( Qt::white );
painter.drawText( rect.x() + 5, rect.bottom() - 4, m_currentLocation ? m_currentLocation->tr() : QString() );
#endif
painter.end();
}
@ -194,7 +197,7 @@ TimeZoneWidget::mousePressEvent( QMouseEvent* event )
using namespace CalamaresUtils::Locale;
const TZZone* closest = nullptr;
for ( const auto* region_p : TZRegion::fromZoneTab() )
for ( const auto* region_p : m_zonesData )
{
const auto* region = dynamic_cast< const TZRegion* >( region_p );
if ( region )
@ -222,6 +225,6 @@ TimeZoneWidget::mousePressEvent( QMouseEvent* event )
// Set zone image and repaint widget
setCurrentLocation( closest );
// Emit signal
emit locationChanged( m_currentLocation );
emit locationChanged( closest );
}
}

View File

@ -31,32 +31,48 @@
#include <QFont>
#include <QWidget>
/** @brief The TimeZoneWidget shows a map and reports where clicks happen
*
* This widget shows a map (unspecified whether it's a whole world map
* or can show regionsvia some kind of internal state). Mouse clicks are
* translated into timezone locations (e.g. the zone for America/New_York).
*
* The current location can be changed programmatically, by name
* or through a pointer to a location. If a pointer is used, take care
* that the pointer is to a zone in the same model as used by the
* widget.
*
* When a location is chosen -- by mouse click or programmatically --
* the locationChanged() signal is emitted with the new location.
*
* NOTE: the widget currently uses the globally cached TZRegion::fromZoneTab()
*/
class TimeZoneWidget : public QWidget
{
Q_OBJECT
public:
using TZZone = CalamaresUtils::Locale::TZZone;
explicit TimeZoneWidget( QWidget* parent = nullptr );
explicit TimeZoneWidget( const CalamaresUtils::Locale::CStringPairList& zones, QWidget* parent = nullptr );
void setCurrentLocation( QString region, QString zone );
public Q_SLOTS:
/** @brief Sets a location by pointer
*
* Pointer should be within the same model as the widget uses.
*/
void setCurrentLocation( const TZZone* location );
const TZZone* currentLocation() { return m_currentLocation; }
signals:
/** @brief The location has changed by mouse click */
void locationChanged( const TZZone* location );
private:
QFont font;
QImage background, pin, currentZoneImage;
TimeZoneImageList timeZoneImages;
const TZZone* m_currentLocation = nullptr; // Not owned by me
QPoint getLocationPosition( const TZZone* l )
{
return timeZoneImages.getLocationPosition( l->longitude(), l->latitude() );
}
const CalamaresUtils::Locale::CStringPairList& m_zonesData;
const TZZone* m_currentLocation = nullptr; // Not owned by me
void paintEvent( QPaintEvent* event );
void mousePressEvent( QMouseEvent* event );

View File

@ -19,74 +19,20 @@
#include "LocaleQmlViewStep.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "geoip/Handler.h"
#include "network/Manager.h"
#include "utils/CalamaresUtilsGui.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include "utils/Yaml.h"
#include "Branding.h"
#include "modulesystem/ModuleManager.h"
#include <QQmlEngine>
#include <QFutureWatcher>
#include <QPixmap>
#include <QVariant>
CALAMARES_PLUGIN_FACTORY_DEFINITION( LocaleQmlViewStepFactory, registerPlugin< LocaleQmlViewStep >(); )
LocaleQmlViewStep::LocaleQmlViewStep( QObject* parent )
: Calamares::QmlViewStep( parent )
, m_config( new Config( this ) )
, m_nextEnabled( false )
, m_geoip( nullptr )
: Calamares::QmlViewStep( parent )
, m_config( std::make_unique< Config >( this ) )
{
emit nextStatusChanged( m_nextEnabled );
}
QObject*
LocaleQmlViewStep::getConfig()
{
return m_config;
}
void
LocaleQmlViewStep::fetchGeoIpTimezone()
{
if ( m_geoip && m_geoip->isValid() )
{
m_startingTimezone = m_geoip->get();
if ( !m_startingTimezone.isValid() )
{
cWarning() << "GeoIP lookup at" << m_geoip->url() << "failed.";
}
}
m_config->setLocaleInfo(m_startingTimezone.first, m_startingTimezone.second, m_localeGenPath);
}
Calamares::RequirementsList LocaleQmlViewStep::checkRequirements()
{
if ( m_geoip && m_geoip->isValid() )
{
auto& network = CalamaresUtils::Network::Manager::instance();
if ( network.hasInternet() )
{
fetchGeoIpTimezone();
}
else
{
if ( network.synchronousPing( m_geoip->url() ) )
{
fetchGeoIpTimezone();
}
}
}
return Calamares::RequirementsList();
return m_config.get();
}
QString
@ -95,17 +41,21 @@ LocaleQmlViewStep::prettyName() const
return tr( "Location" );
}
QString
LocaleQmlViewStep::prettyStatus() const
{
return m_config->prettyStatus();
}
bool
LocaleQmlViewStep::isNextEnabled() const
{
// TODO: should return true
return true;
}
bool
LocaleQmlViewStep::isBackEnabled() const
{
// TODO: should return true (it's weird that you are not allowed to have welcome *after* anything
return true;
}
@ -113,7 +63,6 @@ LocaleQmlViewStep::isBackEnabled() const
bool
LocaleQmlViewStep::isAtBeginning() const
{
// TODO: adjust to "pages" in the QML
return true;
}
@ -121,79 +70,18 @@ LocaleQmlViewStep::isAtBeginning() const
bool
LocaleQmlViewStep::isAtEnd() const
{
// TODO: adjust to "pages" in the QML
return true;
}
Calamares::JobList
LocaleQmlViewStep::jobs() const
{
return m_jobs;
return m_config->createJobs();
}
void LocaleQmlViewStep::onActivate()
void
LocaleQmlViewStep::setConfigurationMap( const QVariantMap& configurationMap )
{
// TODO no sure if it is needed at all or for the abstract class to start something
}
void LocaleQmlViewStep::onLeave()
{
if ( true )
{
m_jobs = m_config->createJobs();
// m_prettyStatus = m_actualWidget->prettyStatus();
auto map = m_config->localesMap();
QVariantMap vm;
for ( auto it = map.constBegin(); it != map.constEnd(); ++it )
{
vm.insert( it.key(), it.value() );
}
Calamares::JobQueue::instance()->globalStorage()->insert( "localeConf", vm );
}
else
{
m_jobs.clear();
Calamares::JobQueue::instance()->globalStorage()->remove( "localeConf" );
}
}
void LocaleQmlViewStep::setConfigurationMap(const QVariantMap& configurationMap)
{
QString region = CalamaresUtils::getString( configurationMap, "region" );
QString zone = CalamaresUtils::getString( configurationMap, "zone" );
if ( !region.isEmpty() && !zone.isEmpty() )
{
m_startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( region, zone );
}
else
{
m_startingTimezone
= CalamaresUtils::GeoIP::RegionZonePair( QStringLiteral( "America" ), QStringLiteral( "New_York" ) );
}
m_localeGenPath = CalamaresUtils::getString( configurationMap, "localeGenPath" );
if ( m_localeGenPath.isEmpty() )
{
m_localeGenPath = QStringLiteral( "/etc/locale.gen" );
}
bool ok = false;
QVariantMap geoip = CalamaresUtils::getSubMap( configurationMap, "geoip", ok );
if ( ok )
{
QString url = CalamaresUtils::getString( geoip, "url" );
QString style = CalamaresUtils::getString( geoip, "style" );
QString selector = CalamaresUtils::getString( geoip, "selector" );
m_geoip = std::make_unique< CalamaresUtils::GeoIP::Handler >( style, url, selector );
if ( !m_geoip->isValid() )
{
cWarning() << "GeoIP Style" << style << "is not recognized.";
}
}
checkRequirements();
Calamares::QmlViewStep::setConfigurationMap( configurationMap ); // call parent implementation last
m_config->setConfigurationMap( configurationMap );
Calamares::QmlViewStep::setConfigurationMap( configurationMap ); // call parent implementation last
}

View File

@ -20,14 +20,10 @@
#define LOCALE_QMLVIEWSTEP_H
#include "Config.h"
#include "geoip/Handler.h"
#include "geoip/Interface.h"
#include "DllMacro.h"
#include "utils/PluginFactory.h"
#include "viewpages/QmlViewStep.h"
#include <DllMacro.h>
#include <QFutureWatcher>
#include <QObject>
#include <memory>
@ -39,6 +35,7 @@ public:
explicit LocaleQmlViewStep( QObject* parent = nullptr );
QString prettyName() const override;
QString prettyStatus() const override;
bool isNextEnabled() const override;
bool isBackEnabled() const override;
@ -47,28 +44,12 @@ public:
bool isAtEnd() const override;
Calamares::JobList jobs() const override;
void onActivate() override;
void onLeave() override;
void setConfigurationMap( const QVariantMap& configurationMap ) override;
QObject* getConfig() override;
virtual Calamares::RequirementsList checkRequirements() override;
private:
// TODO: a generic QML viewstep should return a config object from a method
Config *m_config;
bool m_nextEnabled;
QString m_prettyStatus;
CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone;
QString m_localeGenPath;
Calamares::JobList m_jobs;
std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip;
void fetchGeoIpTimezone();
std::unique_ptr< Config > m_config;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( LocaleQmlViewStepFactory )

View File

@ -74,6 +74,7 @@ Column {
var tz2 = responseJSON.timezoneId
tzText.text = "Timezone: " + tz2
config.setCurrentLocation(tz2)
}
}

View File

@ -32,10 +32,6 @@ Item {
anchors.fill: parent
}
//Needs to come from Locale config
property var confLang: "en_US.UTF8"
property var confLocale: "nl_NL.UTF8"
Rectangle {
id: textArea
x: 28
@ -57,7 +53,7 @@ Item {
width: 240
wrapMode: Text.WordWrap
text: qsTr("<h1>Languages</h1> </br>
The system locale setting affects the language and character set for some command line user interface elements. The current setting is <strong>%1</strong>.").arg(confLang)
The system locale setting affects the language and character set for some command line user interface elements. The current setting is <strong>%1</strong>.").arg(config.currentLanguageCode)
font.pointSize: 10
}
}
@ -76,10 +72,9 @@ Item {
id: list1
focus: true
// bogus entries, need to come from Locale config
model: ["en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8"]
model: config.supportedLocales
currentIndex: 1
currentIndex: -1
highlight: Rectangle {
color: Kirigami.Theme.highlightColor
}
@ -95,17 +90,17 @@ Item {
}
onClicked: {
list1.currentIndex = index
confLang = list1.currentIndex
}
}
}
onCurrentItemChanged: { config.currentLanguageCode = model[currentIndex] } /* This works because model is a stringlist */
}
}
}
}
Column {
id: i18n
id: lc_numeric
x: 430
y: 40
@ -118,7 +113,7 @@ Item {
width: 240
wrapMode: Text.WordWrap
text: qsTr("<h1>Locales</h1> </br>
The system locale setting affects the language and character set for some command line user interface elements. The current setting is <strong>%1</strong>.").arg(confLocale)
The system locale setting affects the numbers and dates format. The current setting is <strong>%1</strong>.").arg(config.currentLCCode)
font.pointSize: 10
}
}
@ -138,10 +133,9 @@ Item {
width: 180; height: 200
focus: true
// bogus entries, need to come from Locale config
model: ["en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8"]
model: config.supportedLocales
currentIndex: 2
currentIndex: -1
highlight: Rectangle {
color: Kirigami.Theme.highlightColor
}
@ -154,11 +148,10 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: {
list2.currentIndex = index
confLocale = list1.currentIndex
}
}
}
onCurrentItemChanged: console.debug(currentIndex)
onCurrentItemChanged: { config.currentLCCode = model[currentIndex]; } /* This works because model is a stringlist */
}
}
}

View File

@ -29,43 +29,13 @@ Page {
width: 800
height: 550
property var confLang: "American English"
property var confLocale: "Nederland"
//Needs to come from .conf/geoip
property var hasInternet: true
function getInt(format) {
var requestURL = "https://example.org/";
var xhr = new XMLHttpRequest;
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status !== 200) {
console.log("Disconnected!!");
var connected = false
hasInternet = connected
return;
}
else {
console.log("Connected!!");
}
}
}
xhr.open("GET", requestURL, true);
xhr.send();
}
Component.onCompleted: {
getInt();
}
Loader {
id: image
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
height: parent.height / 1.28
source: (hasInternet) ? "Map.qml" : "Offline.qml"
// Network is in io.calamares.core
source: Network.hasInternet ? "Map.qml" : "Offline.qml"
}
RowLayout {
@ -95,7 +65,7 @@ Page {
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: qsTr("System language set to %1").arg(confLang)
text: config.currentLanguageStatus
}
Kirigami.Separator {
Layout.fillWidth: true
@ -103,7 +73,7 @@ Page {
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: qsTr("Numbers and dates locale set to %1").arg(confLocale)
text: config.currentLCStatus
}
}
Button {

View File

@ -15,8 +15,6 @@ dbus: true
# Whether /var/lib/dbus/machine-id should be a symlink to /etc/machine-id
# (ignored if dbus is false, or if there is no /etc/machine-id to point to).
dbus-symlink: true
# this is a deprecated form of *dbus-symlink*
symlink: true
# Whether to create an entropy file
entropy: false

View File

@ -4,6 +4,10 @@ $id: https://calamares.io/schemas/machineid
additionalProperties: false
type: object
properties:
"systemd": { type: boolean, default: true }
"dbus": { type: boolean, default: true }
"symlink": { type: boolean, default: true }
systemd: { type: boolean, default: true }
dbus: { type: boolean, default: true }
"dbus-symlink": { type: boolean, default: true }
entropy: { type: boolean, default: false }
"entropy-copy": { type: boolean, default: false }
# Deprecated properties
symlink: { type: boolean, default: true }

View File

@ -9,8 +9,6 @@ calamares_add_plugin( netinstall
PackageModel.cpp
UI
page_netinst.ui
RESOURCES
netinstall.qrc
LINK_PRIVATE_LIBRARIES
calamaresui
Qt5::Network

View File

@ -332,9 +332,7 @@ ChoicePage::setupChoices()
CALAMARES_RETRANSLATE(
m_somethingElseButton->setText( tr( "<strong>Manual partitioning</strong><br/>"
"You can create or resize partitions yourself."
" Having a GPT partition table and <strong>fat32 512Mb /boot partition "
"is a must for UEFI installs</strong>, either use an existing without formatting or create one." ) );
"You can create or resize partitions yourself." ) );
updateSwapChoicesTr( m_eraseSwapChoiceComboBox );
)
}
@ -1444,46 +1442,75 @@ ChoicePage::currentChoice() const
return m_choice;
}
void
ChoicePage::updateNextEnabled()
bool
ChoicePage::calculateNextEnabled() const
{
bool enabled = false;
auto sm_p = m_beforePartitionBarsView ? m_beforePartitionBarsView->selectionModel() : nullptr;
switch ( m_choice )
{
case NoChoice:
enabled = false;
break;
cDebug() << "No partitioning choice";
return false;
case Replace:
case Alongside:
enabled = sm_p && sm_p->currentIndex().isValid();
if ( !( sm_p && sm_p->currentIndex().isValid() ) )
{
cDebug() << "No partition selected";
return false;
}
enabled = true;
break;
case Erase:
case Manual:
enabled = true;
}
if ( m_isEfi &&
( m_choice == Alongside ||
m_choice == Replace ) )
if (!enabled)
{
if ( m_core->efiSystemPartitions().count() == 0 )
enabled = false;
cDebug() << "No valid choice made";
return false;
}
if ( m_choice != Manual &&
m_encryptWidget->isVisible() &&
m_encryptWidget->state() == EncryptWidget::Encryption::Unconfirmed )
enabled = false;
if ( enabled == m_nextEnabled )
return;
if ( m_isEfi && ( m_choice == Alongside || m_choice == Replace ) )
{
if ( m_core->efiSystemPartitions().count() == 0 )
{
cDebug() << "No EFI partition for alongside or replace";
return false;
}
}
m_nextEnabled = enabled;
emit nextStatusChanged( enabled );
if ( m_choice != Manual && m_encryptWidget->isVisible() )
{
switch ( m_encryptWidget->state() )
{
case EncryptWidget::Encryption::Unconfirmed:
cDebug() << "No passphrase provided";
return false;
case EncryptWidget::Encryption::Disabled:
case EncryptWidget::Encryption::Confirmed:
// Checkbox not checked, **or** passphrases match
break;
}
}
return true;
}
void
ChoicePage::updateNextEnabled()
{
bool enabled = calculateNextEnabled();
if ( enabled != m_nextEnabled )
{
m_nextEnabled = enabled;
emit nextStatusChanged( enabled );
}
}
void

View File

@ -126,6 +126,7 @@ private slots:
void onEraseSwapChoiceChanged();
private:
bool calculateNextEnabled() const;
void updateNextEnabled();
void setupChoices();
QComboBox* createBootloaderComboBox( QWidget* parentButton );

View File

@ -91,9 +91,39 @@ EncryptWidget::retranslate()
}
///@brief Give @p label the @p pixmap from the standard-pixmaps
static void
applyPixmap( QLabel* label, CalamaresUtils::ImageType pixmap )
{
label->setFixedWidth( label->height() );
label->setPixmap( CalamaresUtils::defaultPixmap( pixmap, CalamaresUtils::Original, label->size() ) );
}
void
EncryptWidget::updateState()
{
if ( m_ui->m_passphraseLineEdit->isVisible() )
{
QString p1 = m_ui->m_passphraseLineEdit->text();
QString p2 = m_ui->m_confirmLineEdit->text();
if ( p1.isEmpty() && p2.isEmpty() )
{
applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusWarning );
m_ui->m_iconLabel->setToolTip( tr( "Please enter the same passphrase in both boxes." ) );
}
else if ( p1 == p2 )
{
applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusOk );
m_ui->m_iconLabel->setToolTip( QString() );
}
else
{
applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusError );
m_ui->m_iconLabel->setToolTip( tr( "Please enter the same passphrase in both boxes." ) );
}
}
Encryption newState;
if ( m_ui->m_encryptCheckBox->isChecked() )
{
@ -119,14 +149,6 @@ EncryptWidget::updateState()
}
}
///@brief Give @p label the @p pixmap from the standard-pixmaps
static void
applyPixmap( QLabel* label, CalamaresUtils::ImageType pixmap )
{
label->setFixedWidth( label->height() );
label->setPixmap( CalamaresUtils::defaultPixmap( pixmap, CalamaresUtils::Original, label->size() ) );
}
void
EncryptWidget::onPassphraseEdited()
{
@ -135,24 +157,6 @@ EncryptWidget::onPassphraseEdited()
m_ui->m_iconLabel->show();
}
QString p1 = m_ui->m_passphraseLineEdit->text();
QString p2 = m_ui->m_confirmLineEdit->text();
m_ui->m_iconLabel->setToolTip( QString() );
if ( p1.isEmpty() && p2.isEmpty() )
{
m_ui->m_iconLabel->clear();
}
else if ( p1 == p2 )
{
applyPixmap( m_ui->m_iconLabel, CalamaresUtils::Yes );
}
else
{
applyPixmap( m_ui->m_iconLabel, CalamaresUtils::No );
m_ui->m_iconLabel->setToolTip( tr( "Please enter the same passphrase in both boxes." ) );
}
updateState();
}

View File

@ -107,8 +107,8 @@ PartitionViewStep::continueLoading()
m_waitingWidget->deleteLater();
m_waitingWidget = nullptr;
connect( m_core, &PartitionCoreModule::hasRootMountPointChanged, this, &PartitionViewStep::nextStatusChanged );
connect( m_choicePage, &ChoicePage::nextStatusChanged, this, &PartitionViewStep::nextStatusChanged );
connect( m_core, &PartitionCoreModule::hasRootMountPointChanged, this, &PartitionViewStep::nextPossiblyChanged );
connect( m_choicePage, &ChoicePage::nextStatusChanged, this, &PartitionViewStep::nextPossiblyChanged );
}
@ -348,6 +348,11 @@ PartitionViewStep::isNextEnabled() const
return false;
}
void
PartitionViewStep::nextPossiblyChanged(bool)
{
emit nextStatusChanged( isNextEnabled() );
}
bool
PartitionViewStep::isBackEnabled() const

View File

@ -78,6 +78,9 @@ private:
void initPartitionCoreModule();
void continueLoading();
/// "slot" for changes to next-status from the KPMCore and ChoicePage
void nextPossiblyChanged( bool );
PartitionCoreModule* m_core;
QStackedWidget* m_widget;
ChoicePage* m_choicePage;

View File

@ -4,7 +4,6 @@ calamares_add_plugin( preservefiles
TYPE job
EXPORT_MACRO PLUGINDLLEXPORT_PRO
SOURCES
permissions.cpp
PreserveFiles.cpp
LINK_PRIVATE_LIBRARIES
calamares

View File

@ -1,39 +1,28 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2018, Adriaan de Groot <groot@kde.org>
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
* 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/>.
*/
#include "PreserveFiles.h"
#include "permissions.h"
#include "CalamaresVersion.h"
#include "JobQueue.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/CommandList.h"
#include "utils/Logger.h"
#include "utils/Permissions.h"
#include "utils/Units.h"
#include <QFile>
using CalamaresUtils::operator""_MiB;
QString targetPrefix()
QString
targetPrefix()
{
if ( CalamaresUtils::System::instance()->doChroot() )
{
@ -42,9 +31,13 @@ QString targetPrefix()
{
QString r = gs->value( "rootMountPoint" ).toString();
if ( !r.isEmpty() )
{
return r;
}
else
{
cDebug() << "RootMountPoint is empty";
}
}
else
{
@ -55,16 +48,21 @@ QString targetPrefix()
return QLatin1String( "/" );
}
QString atReplacements( QString s )
QString
atReplacements( QString s )
{
Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
QString root( "/" );
QString user;
if ( gs && gs->contains( "rootMountPoint" ) )
{
root = gs->value( "rootMountPoint" ).toString();
}
if ( gs && gs->contains( "username" ) )
{
user = gs->value( "username" ).toString();
}
return s.replace( "@@ROOT@@", root ).replace( "@@USER@@", user );
}
@ -74,9 +72,7 @@ PreserveFiles::PreserveFiles( QObject* parent )
{
}
PreserveFiles::~PreserveFiles()
{
}
PreserveFiles::~PreserveFiles() {}
QString
PreserveFiles::prettyName() const
@ -107,8 +103,7 @@ copy_file( const QString& source, const QString& dest )
{
b = sourcef.read( 1_MiB );
destf.write( b );
}
while ( b.count() > 0 );
} while ( b.count() > 0 );
sourcef.close();
destf.close();
@ -116,14 +111,19 @@ copy_file( const QString& source, const QString& dest )
return true;
}
Calamares::JobResult PreserveFiles::exec()
Calamares::JobResult
PreserveFiles::exec()
{
if ( m_items.isEmpty() )
{
return Calamares::JobResult::error( tr( "No files configured to save for later." ) );
}
QString prefix = targetPrefix();
if ( !prefix.endsWith( '/' ) )
{
prefix.append( '/' );
}
int count = 0;
for ( const auto& it : m_items )
@ -133,37 +133,34 @@ Calamares::JobResult PreserveFiles::exec()
QString dest = prefix + bare_dest;
if ( it.type == ItemType::Log )
{
source = Logger::logFile();
}
if ( it.type == ItemType::Config )
{
if ( Calamares::JobQueue::instance()->globalStorage()->save( dest ) )
{
cWarning() << "Could not write config for" << dest;
}
else
{
++count;
}
}
else if ( source.isEmpty() )
{
cWarning() << "Skipping unnamed source file for" << dest;
}
else
{
if ( copy_file( source, dest ) )
{
if ( it.perm.isValid() )
{
auto s_p = CalamaresUtils::System::instance();
int r;
r = s_p->targetEnvCall( QStringList{ "chown", it.perm.username(), bare_dest } );
if ( r )
cWarning() << "Could not chown target" << bare_dest;
r = s_p->targetEnvCall( QStringList{ "chgrp", it.perm.group(), bare_dest } );
if ( r )
cWarning() << "Could not chgrp target" << bare_dest;
r = s_p->targetEnvCall( QStringList{ "chmod", it.perm.octal(), bare_dest } );
if ( r )
cWarning() << "Could not chmod target" << bare_dest;
if ( !it.perm.apply( CalamaresUtils::System::instance()->targetPath( bare_dest ) ) )
{
cWarning() << "Could not set attributes of" << bare_dest;
}
}
++count;
@ -171,12 +168,13 @@ Calamares::JobResult PreserveFiles::exec()
}
}
return count == m_items.count() ?
Calamares::JobResult::ok() :
Calamares::JobResult::error( tr( "Not all of the configured files could be preserved." ) );
return count == m_items.count()
? Calamares::JobResult::ok()
: Calamares::JobResult::error( tr( "Not all of the configured files could be preserved." ) );
}
void PreserveFiles::setConfigurationMap(const QVariantMap& configurationMap)
void
PreserveFiles::setConfigurationMap( const QVariantMap& configurationMap )
{
auto files = configurationMap[ "files" ];
if ( !files.isValid() )
@ -193,7 +191,9 @@ void PreserveFiles::setConfigurationMap(const QVariantMap& configurationMap)
QString defaultPermissions = configurationMap[ "perm" ].toString();
if ( defaultPermissions.isEmpty() )
{
defaultPermissions = QStringLiteral( "root:root:0400" );
}
QVariantList l = files.toList();
unsigned int c = 0;
@ -203,22 +203,23 @@ void PreserveFiles::setConfigurationMap(const QVariantMap& configurationMap)
{
QString filename = li.toString();
if ( !filename.isEmpty() )
m_items.append( Item{ filename, filename, Permissions( defaultPermissions ), ItemType::Path } );
m_items.append( Item { filename, filename, CalamaresUtils::Permissions( defaultPermissions ), ItemType::Path } );
else
{
cDebug() << "Empty filename for preservefiles, item" << c;
}
}
else if ( li.type() == QVariant::Map )
{
const auto map = li.toMap();
QString dest = map[ "dest" ].toString();
QString from = map[ "from" ].toString();
ItemType t =
( from == "log" ) ? ItemType::Log :
( from == "config" ) ? ItemType::Config :
ItemType::None;
ItemType t = ( from == "log" ) ? ItemType::Log : ( from == "config" ) ? ItemType::Config : ItemType::None;
QString perm = map[ "perm" ].toString();
if ( perm.isEmpty() )
{
perm = defaultPermissions;
}
if ( dest.isEmpty() )
{
@ -230,15 +231,16 @@ void PreserveFiles::setConfigurationMap(const QVariantMap& configurationMap)
}
else
{
m_items.append( Item{ QString(), dest, Permissions( perm ), t } );
m_items.append( Item { QString(), dest, CalamaresUtils::Permissions( perm ), t } );
}
}
else
{
cDebug() << "Invalid type for preservefiles, item" << c;
}
++c;
}
}
CALAMARES_PLUGIN_FACTORY_DEFINITION( PreserveFilesFactory, registerPlugin<PreserveFiles>(); )
CALAMARES_PLUGIN_FACTORY_DEFINITION( PreserveFilesFactory, registerPlugin< PreserveFiles >(); )

View File

@ -1,35 +1,23 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2018, Adriaan de Groot <groot@kde.org>
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
* 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/>.
*/
#ifndef PRESERVEFILES_H
#define PRESERVEFILES_H
#include "CppJob.h"
#include "DllMacro.h"
#include "utils/Permissions.h"
#include "utils/PluginFactory.h"
#include <QList>
#include <QObject>
#include <QVariantMap>
#include "CppJob.h"
#include "DllMacro.h"
#include "utils/PluginFactory.h"
#include "permissions.h"
class PLUGINDLLEXPORT PreserveFiles : public Calamares::CppJob
{
Q_OBJECT
@ -40,15 +28,15 @@ class PLUGINDLLEXPORT PreserveFiles : public Calamares::CppJob
Path,
Log,
Config
} ;
};
struct Item
{
QString source;
QString dest;
Permissions perm;
CalamaresUtils::Permissions perm;
ItemType type;
} ;
};
using ItemList = QList< Item >;
@ -68,4 +56,4 @@ private:
CALAMARES_PLUGIN_FACTORY_DECLARATION( PreserveFilesFactory )
#endif // PRESERVEFILES_H
#endif // PRESERVEFILES_H

View File

@ -1,75 +0,0 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright (C) 2018 Scott Harvey <scott@spharvey.me>
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QString>
#include <QStringList>
#include "permissions.h"
Permissions::Permissions() :
m_username(),
m_group(),
m_valid(false),
m_value(0)
{
}
Permissions::Permissions(QString p) : Permissions()
{
parsePermissions(p);
}
void Permissions::parsePermissions(const QString& p) {
QStringList segments = p.split(":");
if (segments.length() != 3) {
m_valid = false;
return;
}
if (segments[0].isEmpty() || segments[1].isEmpty()) {
m_valid = false;
return;
}
bool ok;
int octal = segments[2].toInt(&ok, 8);
if (!ok || octal == 0) {
m_valid = false;
return;
} else {
m_value = octal;
}
// We have exactly three segments and the third is valid octal,
// so we can declare the string valid and set the user and group names
m_valid = true;
m_username = segments[0];
m_group = segments[1];
return;
}

View File

@ -1,62 +0,0 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright (C) 2018 Scott Harvey <scott@spharvey.me>
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PERMISSIONS_H
#define PERMISSIONS_H
#include <QString>
/**
* @brief The Permissions class takes a QString @p in the form of
* <user>:<group>:<permissions>, checks it for validity, and makes the three
* components available indivdually.
*/
class Permissions
{
public:
/** @brief Constructor
*
* Splits the string @p at the colon (":") into separate elements for
* <user>, <group>, and <value> (permissions), where <value> is returned as
* an **octal** integer.
*/
Permissions(QString p);
/** @brief Default constructor of an invalid Permissions. */
Permissions();
bool isValid() const { return m_valid; }
QString username() const { return m_username; }
QString group() const { return m_group; }
int value() const { return m_value; }
QString octal() const { return QString::number( m_value, 8 ); }
private:
void parsePermissions(QString const &p);
QString m_username;
QString m_group;
bool m_valid;
int m_value;
};
#endif // PERMISSIONS_H

View File

@ -28,6 +28,7 @@ calamares_add_plugin( users
UsersPage.cpp
SetHostNameJob.cpp
CheckPWQuality.cpp
Config.cpp
UI
page_usersetup.ui
RESOURCES
@ -43,17 +44,31 @@ calamares_add_plugin( users
calamares_add_test(
userspasswordtest
SOURCES
PasswordTests.cpp
TestPasswordJob.cpp
SetPasswordJob.cpp
LIBRARIES
${CRYPT_LIBRARIES}
)
calamares_add_test(
userstest
userscreatetest
SOURCES
Tests.cpp
TestCreateUserJob.cpp
CreateUserJob.cpp
)
calamares_add_test(
usershostnametest
SOURCES
TestSetHostNameJob.cpp
SetHostNameJob.cpp
LIBRARIES
Qt5::DBus
)
calamares_add_test(
userstest
SOURCES
Tests.cpp
Config.cpp
)

View File

@ -55,7 +55,7 @@ DEFINE_CHECK_FUNC( minLength )
{
cDebug() << Logger::SubEntry << "minLength set to" << minLength;
checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too short" ); },
[ minLength ]( const QString& s ) { return s.length() >= minLength; },
[minLength]( const QString& s ) { return s.length() >= minLength; },
PasswordCheck::Weight( 10 ) ) );
}
}
@ -71,7 +71,7 @@ DEFINE_CHECK_FUNC( maxLength )
{
cDebug() << Logger::SubEntry << "maxLength set to" << maxLength;
checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too long" ); },
[ maxLength ]( const QString& s ) { return s.length() <= maxLength; },
[maxLength]( const QString& s ) { return s.length() <= maxLength; },
PasswordCheck::Weight( 10 ) ) );
}
}
@ -349,8 +349,8 @@ DEFINE_CHECK_FUNC( libpwquality )
/* Something actually added? */
if ( requirement_count )
{
checks.push_back( PasswordCheck( [ settings ]() { return settings->explanation(); },
[ settings ]( const QString& s ) {
checks.push_back( PasswordCheck( [settings]() { return settings->explanation(); },
[settings]( const QString& s ) {
int r = settings->check( s );
if ( r < 0 )
{

View File

@ -0,0 +1,380 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
* 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/>.
*/
#include "Config.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/Logger.h"
#include "utils/String.h"
#include "utils/Variant.h"
#include <QFile>
#include <QRegExp>
static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" );
static constexpr const int USERNAME_MAX_LENGTH = 31;
static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" );
static constexpr const int HOSTNAME_MIN_LENGTH = 2;
static constexpr const int HOSTNAME_MAX_LENGTH = 63;
Config::Config( QObject* parent )
: QObject( parent )
{
}
Config::~Config() {}
void
Config::setUserShell( const QString& shell )
{
if ( !shell.isEmpty() && !shell.startsWith( '/' ) )
{
cWarning() << "User shell" << shell << "is not an absolute path.";
return;
}
// The shell is put into GS because the CreateUser job expects it there
Calamares::JobQueue::instance()->globalStorage()->insert( "userShell", shell );
}
static inline void
insertInGlobalStorage( const QString& key, const QString& group )
{
auto* gs = Calamares::JobQueue::instance()->globalStorage();
if ( !gs || group.isEmpty() )
{
return;
}
gs->insert( key, group );
}
void
Config::setAutologinGroup( const QString& group )
{
insertInGlobalStorage( QStringLiteral( "autologinGroup" ), group );
emit autologinGroupChanged( group );
}
void
Config::setSudoersGroup( const QString& group )
{
insertInGlobalStorage( QStringLiteral( "sudoersGroup" ), group );
emit sudoersGroupChanged( group );
}
void
Config::setLoginName( const QString& login )
{
if ( login != m_loginName )
{
Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
if ( login.isEmpty() )
{
gs->remove( "username" );
}
else
{
gs->insert( "username", login );
}
m_customLoginName = !login.isEmpty();
m_loginName = login;
emit loginNameChanged( login );
emit loginNameStatusChanged( loginNameStatus() );
}
}
const QStringList&
Config::forbiddenLoginNames()
{
static QStringList forbidden { "root" };
return forbidden;
}
QString
Config::loginNameStatus() const
{
// An empty login is "ok", even if it isn't really
if ( m_loginName.isEmpty() )
{
return QString();
}
if ( m_loginName.length() > USERNAME_MAX_LENGTH )
{
return tr( "Your username is too long." );
}
for ( const QString& badName : forbiddenLoginNames() )
{
if ( 0 == QString::compare( badName, m_loginName, Qt::CaseSensitive ) )
{
return tr( "'%1' is not allowed as username." ).arg( badName );
}
}
QRegExp validateFirstLetter( "^[a-z_]" );
if ( validateFirstLetter.indexIn( m_loginName ) != 0 )
{
return tr( "Your username must start with a lowercase letter or underscore." );
}
if ( !USERNAME_RX.exactMatch( m_loginName ) )
{
return tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." );
}
return QString();
}
void
Config::setHostName( const QString& host )
{
if ( host != m_hostName )
{
Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
if ( host.isEmpty() )
{
gs->remove( "hostname" );
}
else
{
gs->insert( "hostname", host );
}
m_customHostName = !host.isEmpty();
m_hostName = host;
emit hostNameChanged( host );
emit hostNameStatusChanged( hostNameStatus() );
}
}
const QStringList&
Config::forbiddenHostNames()
{
static QStringList forbidden { "localhost" };
return forbidden;
}
QString
Config::hostNameStatus() const
{
// An empty hostname is "ok", even if it isn't really
if ( m_hostName.isEmpty() )
{
return QString();
}
if ( m_hostName.length() < HOSTNAME_MIN_LENGTH )
{
return tr( "Your hostname is too short." );
}
if ( m_hostName.length() > HOSTNAME_MAX_LENGTH )
{
return tr( "Your hostname is too long." );
}
for ( const QString& badName : forbiddenHostNames() )
{
if ( 0 == QString::compare( badName, m_hostName, Qt::CaseSensitive ) )
{
return tr( "'%1' is not allowed as hostname." ).arg( badName );
}
}
if ( !HOSTNAME_RX.exactMatch( m_hostName ) )
{
return tr( "Only letters, numbers, underscore and hyphen are allowed." );
}
return QString();
}
/** @brief Guess the machine's name
*
* If there is DMI data, use that; otherwise, just call the machine "-pc".
* Reads the DMI data just once.
*/
static QString
guessProductName()
{
static bool tried = false;
static QString dmiProduct;
if ( !tried )
{
// yes validateHostnameText() but these files can be a mess
QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive );
QFile dmiFile( QStringLiteral( "/sys/devices/virtual/dmi/id/product_name" ) );
if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) )
{
dmiProduct = QString::fromLocal8Bit( dmiFile.readAll().simplified().data() )
.toLower()
.replace( dmirx, " " )
.remove( ' ' );
}
if ( dmiProduct.isEmpty() )
{
dmiProduct = QStringLiteral( "pc" );
}
tried = true;
}
return dmiProduct;
}
static QString
makeLoginNameSuggestion( const QStringList& parts )
{
if ( parts.isEmpty() || parts.first().isEmpty() )
{
return QString();
}
QString usernameSuggestion = parts.first();
for ( int i = 1; i < parts.length(); ++i )
{
if ( !parts.value( i ).isEmpty() )
{
usernameSuggestion.append( parts.value( i ).at( 0 ) );
}
}
return USERNAME_RX.indexIn( usernameSuggestion ) != -1 ? usernameSuggestion : QString();
}
static QString
makeHostnameSuggestion( const QStringList& parts )
{
static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" );
if ( parts.isEmpty() || parts.first().isEmpty() )
{
return QString();
}
QString productName = guessProductName();
QString hostnameSuggestion = QStringLiteral( "%1-%2" ).arg( parts.first() ).arg( productName );
return HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 ? hostnameSuggestion : QString();
}
void
Config::setFullName( const QString& name )
{
if ( name.isEmpty() && !m_fullName.isEmpty() )
{
if ( !m_customHostName )
{
setHostName( name );
}
if ( !m_customLoginName )
{
setLoginName( name );
}
m_fullName = name;
emit fullNameChanged( name );
}
if ( name != m_fullName )
{
m_fullName = name;
emit fullNameChanged( name );
// Build login and hostname, if needed
QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive );
QString cleanName = CalamaresUtils::removeDiacritics( name ).toLower().replace( rx, " " ).simplified();
QStringList cleanParts = cleanName.split( ' ' );
if ( !m_customLoginName )
{
QString login = makeLoginNameSuggestion( cleanParts );
if ( !login.isEmpty() && login != m_loginName )
{
m_loginName = login;
emit loginNameChanged( login );
emit loginNameStatusChanged( loginNameStatus() );
}
}
if ( !m_customHostName )
{
QString hostname = makeHostnameSuggestion( cleanParts );
if ( !hostname.isEmpty() && hostname != m_hostName )
{
m_hostName = hostname;
emit hostNameChanged( hostname );
emit hostNameStatusChanged( hostNameStatus() );
}
}
}
}
void
Config::setAutoLogin( bool b )
{
if ( b != m_doAutoLogin )
{
Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
if ( b )
{
gs->insert( "autologinUser", loginName() );
}
else
{
gs->remove( "autologinUser" );
}
m_doAutoLogin = b;
emit autoLoginChanged( b );
}
}
STATICTEST inline void
setConfigurationDefaultGroups( const QVariantMap& map, QStringList& defaultGroups )
{
// '#' is not a valid group name; use that to distinguish an empty-list
// in the configuration (which is a legitimate, if unusual, choice)
// from a bad or missing configuration value.
defaultGroups = CalamaresUtils::getStringList( map, QStringLiteral( "defaultGroups" ), QStringList { "#" } );
if ( defaultGroups.contains( QStringLiteral( "#" ) ) )
{
cWarning() << "Using fallback groups. Please check *defaultGroups* in users.conf";
defaultGroups = QStringList { "lp", "video", "network", "storage", "wheel", "audio" };
}
}
void
Config::setConfigurationMap( const QVariantMap& configurationMap )
{
QString shell( QLatin1String( "/bin/bash" ) ); // as if it's not set at all
if ( configurationMap.contains( "userShell" ) )
{
shell = CalamaresUtils::getString( configurationMap, "userShell" );
}
// Now it might be explicitly set to empty, which is ok
setUserShell( shell );
setAutologinGroup( CalamaresUtils::getString( configurationMap, "autologinGroup" ) );
setSudoersGroup( CalamaresUtils::getString( configurationMap, "sudoersGroup" ) );
setConfigurationDefaultGroups( configurationMap, m_defaultGroups );
m_doAutoLogin = CalamaresUtils::getBool( configurationMap, "doAutologin", false );
m_writeRootPassword = CalamaresUtils::getBool( configurationMap, "setRootPassword", true );
Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", m_writeRootPassword );
}

138
src/modules/users/Config.h Normal file
View File

@ -0,0 +1,138 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
* License-Filename: LICENSE
*
* 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/>.
*/
#ifndef USERS_CONFIG_H
#define USERS_CONFIG_H
#include <QObject>
#include <QVariantMap>
class Config : public QObject
{
Q_OBJECT
Q_PROPERTY( QString userShell READ userShell WRITE setUserShell NOTIFY userShellChanged )
Q_PROPERTY( QString autologinGroup READ autologinGroup WRITE setAutologinGroup NOTIFY autologinGroupChanged )
Q_PROPERTY( QString sudoersGroup READ sudoersGroup WRITE setSudoersGroup NOTIFY sudoersGroupChanged )
Q_PROPERTY( bool doAutoLogin READ doAutoLogin WRITE setAutoLogin NOTIFY autoLoginChanged )
Q_PROPERTY( QString fullName READ fullName WRITE setFullName NOTIFY fullNameChanged )
Q_PROPERTY( QString loginName READ loginName WRITE setLoginName NOTIFY loginNameChanged )
Q_PROPERTY( QString loginNameStatus READ loginNameStatus NOTIFY loginNameStatusChanged )
Q_PROPERTY( QString hostName READ hostName WRITE setHostName NOTIFY hostNameChanged )
Q_PROPERTY( QString hostNameStatus READ hostNameStatus NOTIFY hostNameStatusChanged )
public:
Config( QObject* parent = nullptr );
~Config();
void setConfigurationMap( const QVariantMap& );
/** @brief Full path to the user's shell executable
*
* Typically this will be /bin/bash, but it can be set from
* the config file with the *userShell* setting.
*/
QString userShell() const { return m_userShell; }
/// The group of which auto-login users must be a member
QString autologinGroup() const { return m_autologinGroup; }
/// The group of which users who can "sudo" must be a member
QString sudoersGroup() const { return m_sudoersGroup; }
/// The full (GECOS) name of the user
QString fullName() const { return m_fullName; }
/// The login name of the user
QString loginName() const { return m_loginName; }
/// Status message about login -- empty for "ok"
QString loginNameStatus() const;
/// The host name (name for the system)
QString hostName() const { return m_hostName; }
/// Status message about hostname -- empty for "ok"
QString hostNameStatus() const;
/// Should the user be automatically logged-in?
bool doAutoLogin() const { return m_doAutoLogin; }
/// Should the root password be written (if false, no password is set and the root account is disabled for login)
bool writeRootPassword() const { return m_writeRootPassword; }
const QStringList& defaultGroups() const { return m_defaultGroups; }
static const QStringList& forbiddenLoginNames();
static const QStringList& forbiddenHostNames();
public Q_SLOTS:
/** @brief Sets the user's shell if possible
*
* If the path is empty, that's ok: no shell will be explicitly set,
* so the user will get whatever shell is set to default in the target.
*
* The given non-empty @p path must be an absolute path (for use inside
* the target system!); if it is not, the shell is not changed.
*/
void setUserShell( const QString& path );
/// Sets the autologin group; empty is ignored
void setAutologinGroup( const QString& group );
/// Sets the sudoer group; empty is ignored
void setSudoersGroup( const QString& group );
/// Sets the full name, may guess a loginName
void setFullName( const QString& name );
/// Sets the login name (flags it as "custom")
void setLoginName( const QString& login );
/// Sets the host name (flags it as "custom")
void setHostName( const QString& host );
/// Sets the autologin flag
void setAutoLogin( bool b );
signals:
void userShellChanged( const QString& );
void autologinGroupChanged( const QString& );
void sudoersGroupChanged( const QString& );
void fullNameChanged( const QString& );
void loginNameChanged( const QString& );
void loginNameStatusChanged( const QString& );
void hostNameChanged( const QString& );
void hostNameStatusChanged( const QString& );
void autoLoginChanged( bool );
private:
QStringList m_defaultGroups;
QString m_userShell;
QString m_autologinGroup;
QString m_sudoersGroup;
QString m_fullName;
QString m_loginName;
QString m_hostName;
bool m_doAutoLogin = false;
bool m_writeRootPassword = true;
bool m_customLoginName = false;
bool m_customHostName = false;
};
#endif

View File

@ -12,6 +12,7 @@
#include "JobQueue.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include "utils/Permissions.h"
#include <QDateTime>
#include <QDir>
@ -54,6 +55,109 @@ CreateUserJob::prettyStatusMessage() const
return tr( "Creating user %1." ).arg( m_userName );
}
STATICTEST QStringList
groupsInTargetSystem( const QDir& targetRoot )
{
QFileInfo groupsFi( targetRoot.absoluteFilePath( "etc/group" ) );
QFile groupsFile( groupsFi.absoluteFilePath() );
if ( !groupsFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
{
return QStringList();
}
QString groupsData = QString::fromLocal8Bit( groupsFile.readAll() );
QStringList groupsLines = groupsData.split( '\n' );
QStringList::iterator it = groupsLines.begin();
while ( it != groupsLines.end() )
{
if ( it->startsWith( '#' ) )
{
it = groupsLines.erase( it );
continue;
}
int indexOfFirstToDrop = it->indexOf( ':' );
if ( indexOfFirstToDrop < 1 )
{
it = groupsLines.erase( it );
continue;
}
it->truncate( indexOfFirstToDrop );
++it;
}
return groupsLines;
}
static void
ensureGroupsExistInTarget( const QStringList& wantedGroups, const QStringList& availableGroups )
{
for ( const QString& group : wantedGroups )
{
if ( !availableGroups.contains( group ) )
{
#ifdef __FreeBSD__
CalamaresUtils::System::instance()->targetEnvCall( { "pw", "groupadd", "-n", group } );
#else
CalamaresUtils::System::instance()->targetEnvCall( { "groupadd", group } );
#endif
}
}
}
static Calamares::JobResult
createUser( const QString& loginName, const QString& fullName, const QString& shell )
{
QStringList useraddCommand;
#ifdef __FreeBSD__
useraddCommand << "pw"
<< "useradd"
<< "-n" << loginName << "-m"
<< "-c" << fullName;
if ( !shell.isEmpty() )
{
useraddCommand << "-s" << shell;
}
#else
useraddCommand << "useradd"
<< "-m"
<< "-U";
if ( !shell.isEmpty() )
{
useraddCommand << "-s" << shell;
}
useraddCommand << "-c" << fullName;
useraddCommand << loginName;
#endif
auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( useraddCommand );
if ( commandResult.getExitCode() )
{
cError() << "useradd failed" << commandResult.getExitCode();
return commandResult.explainProcess( useraddCommand, std::chrono::seconds( 10 ) /* bogus timeout */ );
}
return Calamares::JobResult::ok();
}
static Calamares::JobResult
setUserGroups( const QString& loginName, const QStringList& groups )
{
QStringList setgroupsCommand;
#ifdef __FreeBSD__
setgroupsCommand << "pw"
<< "usermod"
<< "-n" << loginName << "-G" << groups.join( ',' );
#else
setgroupsCommand << "usermod"
<< "-aG" << groups.join( ',' ) << loginName;
#endif
auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( setgroupsCommand );
if ( commandResult.getExitCode() )
{
cError() << "usermod failed" << commandResult.getExitCode();
return commandResult.explainProcess( setgroupsCommand, std::chrono::seconds( 10 ) /* bogus timeout */ );
}
return Calamares::JobResult::ok();
}
Calamares::JobResult
CreateUserJob::exec()
@ -65,65 +169,37 @@ CreateUserJob::exec()
{
cDebug() << "[CREATEUSER]: preparing sudoers";
QFileInfo sudoersFi( destDir.absoluteFilePath( "etc/sudoers.d/10-installer" ) );
QString sudoersLine = QString( "%%1 ALL=(ALL) ALL\n" ).arg( gs->value( "sudoersGroup" ).toString() );
auto fileResult
= CalamaresUtils::System::instance()->createTargetFile( QStringLiteral( "/etc/sudoers.d/10-installer" ),
sudoersLine.toUtf8().constData(),
CalamaresUtils::System::WriteMode::Overwrite );
if ( !sudoersFi.absoluteDir().exists() )
if ( fileResult )
{
return Calamares::JobResult::error( tr( "Sudoers dir is not writable." ) );
if ( CalamaresUtils::Permissions::apply( fileResult.path(), 0440 ) )
{
return Calamares::JobResult::error( tr( "Cannot chmod sudoers file." ) );
}
}
QFile sudoersFile( sudoersFi.absoluteFilePath() );
if ( !sudoersFile.open( QIODevice::WriteOnly | QIODevice::Text ) )
else
{
return Calamares::JobResult::error( tr( "Cannot create sudoers file for writing." ) );
}
QString sudoersGroup = gs->value( "sudoersGroup" ).toString();
QTextStream sudoersOut( &sudoersFile );
sudoersOut << QString( "%%1 ALL=(ALL) ALL\n" ).arg( sudoersGroup );
if ( QProcess::execute( "chmod", { "440", sudoersFi.absoluteFilePath() } ) )
return Calamares::JobResult::error( tr( "Cannot chmod sudoers file." ) );
}
cDebug() << "[CREATEUSER]: preparing groups";
QFileInfo groupsFi( destDir.absoluteFilePath( "etc/group" ) );
QFile groupsFile( groupsFi.absoluteFilePath() );
if ( !groupsFile.open( QIODevice::ReadOnly | QIODevice::Text ) )
QStringList availableGroups = groupsInTargetSystem( destDir );
QStringList groupsForThisUser = m_defaultGroups;
if ( m_autologin && gs->contains( "autologinGroup" ) && !gs->value( "autologinGroup" ).toString().isEmpty() )
{
return Calamares::JobResult::error( tr( "Cannot open groups file for reading." ) );
}
QString groupsData = QString::fromLocal8Bit( groupsFile.readAll() );
QStringList groupsLines = groupsData.split( '\n' );
for ( QStringList::iterator it = groupsLines.begin(); it != groupsLines.end(); ++it )
{
int indexOfFirstToDrop = it->indexOf( ':' );
it->truncate( indexOfFirstToDrop );
groupsForThisUser << gs->value( "autologinGroup" ).toString();
}
ensureGroupsExistInTarget( m_defaultGroups, availableGroups );
for ( const QString& group : m_defaultGroups )
{
if ( !groupsLines.contains( group ) )
{
CalamaresUtils::System::instance()->targetEnvCall( { "groupadd", group } );
}
}
QString defaultGroups = m_defaultGroups.join( ',' );
if ( m_autologin )
{
QString autologinGroup;
if ( gs->contains( "autologinGroup" ) && !gs->value( "autologinGroup" ).toString().isEmpty() )
{
autologinGroup = gs->value( "autologinGroup" ).toString();
CalamaresUtils::System::instance()->targetEnvCall( { "groupadd", autologinGroup } );
defaultGroups.append( QString( ",%1" ).arg( autologinGroup ) );
}
}
// If we're looking to reuse the contents of an existing /home
// If we're looking to reuse the contents of an existing /home.
// This GS setting comes from the **partitioning** module.
if ( gs->value( "reuseHome" ).toBool() )
{
QString shellFriendlyHome = "/home/" + m_userName;
@ -133,6 +209,7 @@ CreateUserJob::exec()
QString backupDirName = "dotfiles_backup_" + QDateTime::currentDateTime().toString( "yyyy-MM-dd_HH-mm-ss" );
existingHome.mkdir( backupDirName );
// We need the extra `sh -c` here to ensure that we can expand the shell globs
CalamaresUtils::System::instance()->targetEnvCall(
{ "sh", "-c", "mv -f " + shellFriendlyHome + "/.* " + shellFriendlyHome + "/" + backupDirName } );
}
@ -140,33 +217,21 @@ CreateUserJob::exec()
cDebug() << "[CREATEUSER]: creating user";
QStringList useradd { "useradd", "-m", "-U" };
QString shell = gs->value( "userShell" ).toString();
if ( !shell.isEmpty() )
auto useraddResult = createUser( m_userName, m_fullName, gs->value( "userShell" ).toString() );
if ( !useraddResult )
{
useradd << "-s" << shell;
}
useradd << "-c" << m_fullName;
useradd << m_userName;
auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( useradd );
if ( commandResult.getExitCode() )
{
cError() << "useradd failed" << commandResult.getExitCode();
return commandResult.explainProcess( useradd, std::chrono::seconds( 10 ) /* bogus timeout */ );
return useraddResult;
}
commandResult
= CalamaresUtils::System::instance()->targetEnvCommand( { "usermod", "-aG", defaultGroups, m_userName } );
if ( commandResult.getExitCode() )
auto usergroupsResult = setUserGroups( m_userName, groupsForThisUser );
if ( !usergroupsResult )
{
cError() << "usermod failed" << commandResult.getExitCode();
return commandResult.explainProcess( "usermod", std::chrono::seconds( 10 ) /* bogus timeout */ );
return usergroupsResult;
}
QString userGroup = QString( "%1:%2" ).arg( m_userName ).arg( m_userName );
QString homeDir = QString( "/home/%1" ).arg( m_userName );
commandResult = CalamaresUtils::System::instance()->targetEnvCommand( { "chown", "-R", userGroup, homeDir } );
auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( { "chown", "-R", userGroup, homeDir } );
if ( commandResult.getExitCode() )
{
cError() << "chown failed" << commandResult.getExitCode();

View File

@ -1,36 +0,0 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2017, Adriaan de Groot <groot@kde.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/>.
*/
#ifndef PASSWORDTESTS_H
#define PASSWORDTESTS_H
#include <QObject>
class PasswordTests : public QObject
{
Q_OBJECT
public:
PasswordTests();
~PasswordTests() override;
private Q_SLOTS:
void initTestCase();
void testSalt();
};
#endif

View File

@ -0,0 +1,79 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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/>.
*/
#include "CreateUserJob.h"
#include "utils/Logger.h"
#include <QDir>
#include <QtTest/QtTest>
// Implementation details
extern QStringList groupsInTargetSystem( const QDir& targetRoot ); // CreateUserJob
class CreateUserTests : public QObject
{
Q_OBJECT
public:
CreateUserTests();
virtual ~CreateUserTests() {}
private Q_SLOTS:
void initTestCase();
void testReadGroup();
};
CreateUserTests::CreateUserTests() {}
void
CreateUserTests::initTestCase()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
cDebug() << "Users test started.";
}
void
CreateUserTests::testReadGroup()
{
QDir root( "/" );
QVERIFY( root.exists() );
// Get the groups in the host system
QStringList groups = groupsInTargetSystem( root );
QVERIFY( groups.count() > 2 );
#ifdef __FreeBSD__
QVERIFY( groups.contains( QStringLiteral( "wheel" ) ) );
#else
QVERIFY( groups.contains( QStringLiteral( "root" ) ) );
#endif
QVERIFY( groups.contains( QStringLiteral( "sys" ) ) );
for ( const QString& s : groups )
{
QVERIFY( !s.isEmpty() );
QVERIFY( !s.contains( '#' ) );
}
}
QTEST_GUILESS_MAIN( CreateUserTests )
#include "utils/moc-warnings.h"
#include "TestCreateUserJob.moc"

View File

@ -1,6 +1,7 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2017, Adriaan de Groot <groot@kde.org>
* SPDX-FileCopyrightText: 2017 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -18,15 +19,23 @@
#include "SetPasswordJob.h"
#include "PasswordTests.h"
#include <QtTest/QtTest>
QTEST_GUILESS_MAIN( PasswordTests )
class PasswordTests : public QObject
{
Q_OBJECT
public:
PasswordTests();
~PasswordTests() override;
PasswordTests::PasswordTests() { }
private Q_SLOTS:
void initTestCase();
void testSalt();
};
PasswordTests::~PasswordTests() { }
PasswordTests::PasswordTests() {}
PasswordTests::~PasswordTests() {}
void
PasswordTests::initTestCase()
@ -48,3 +57,9 @@ PasswordTests::testSalt()
QVERIFY( s.endsWith( '$' ) );
qDebug() << "Obtained salt" << s;
}
QTEST_GUILESS_MAIN( PasswordTests )
#include "utils/moc-warnings.h"
#include "TestPasswordJob.moc"

View File

@ -0,0 +1,146 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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/>.
*/
#include "SetHostNameJob.h"
// Implementation details
extern bool setFileHostname( const QString& );
extern bool writeFileEtcHosts( const QString& );
extern bool setSystemdHostname( const QString& );
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include "utils/Yaml.h"
#include <QTemporaryDir>
#include <QtTest/QtTest>
class UsersTests : public QObject
{
Q_OBJECT
public:
UsersTests();
virtual ~UsersTests() {}
private Q_SLOTS:
void initTestCase();
void testEtcHostname();
void testEtcHosts();
void testHostnamed();
void cleanup();
private:
QTemporaryDir m_dir;
};
UsersTests::UsersTests()
: m_dir( QStringLiteral( "/tmp/calamares-usertest" ) )
{
}
void
UsersTests::initTestCase()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
cDebug() << "Users test started.";
cDebug() << "Test dir" << m_dir.path();
// Ensure we have a system object, expect it to be a "bogus" one
CalamaresUtils::System* system = CalamaresUtils::System::instance();
QVERIFY( system );
QVERIFY( system->doChroot() );
// Ensure we have a system-wide GlobalStorage with /tmp as root
if ( !Calamares::JobQueue::instance() )
{
cDebug() << "Creating new JobQueue";
(void)new Calamares::JobQueue();
}
Calamares::GlobalStorage* gs
= Calamares::JobQueue::instance() ? Calamares::JobQueue::instance()->globalStorage() : nullptr;
QVERIFY( gs );
gs->insert( "rootMountPoint", m_dir.path() );
}
void
UsersTests::testEtcHostname()
{
cDebug() << "Test dir" << m_dir.path();
QVERIFY( QFile::exists( m_dir.path() ) );
QVERIFY( !QFile::exists( m_dir.filePath( "etc" ) ) );
// Doesn't create intermediate directories
QVERIFY( !setFileHostname( QStringLiteral( "tubophone.calamares.io" ) ) );
QVERIFY( CalamaresUtils::System::instance()->createTargetDirs( "/etc" ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc" ) ) );
// Does write the file
QVERIFY( setFileHostname( QStringLiteral( "tubophone.calamares.io" ) ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc/hostname" ) ) );
// 22 for the test string, above, and 1 for the newline
QCOMPARE( QFileInfo( m_dir.filePath( "etc/hostname" ) ).size(), 22 + 1 );
}
void
UsersTests::testEtcHosts()
{
// Assume previous tests did their work
QVERIFY( QFile::exists( m_dir.path() ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc" ) ) );
QVERIFY( writeFileEtcHosts( QStringLiteral( "tubophone.calamares.io" ) ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc/hosts" ) ) );
// The skeleton contains %1 which has the hostname substituted in, so we lose two,
// and the rest of the blabla is 150 (according to Python)
QCOMPARE( QFileInfo( m_dir.filePath( "etc/hosts" ) ).size(), 150 + 22 - 2 );
}
void
UsersTests::testHostnamed()
{
// Since the service might not be running (e.g. non-systemd systems,
// FreeBSD, docker, ..) we're not going to fail a test here.
// There's also the permissions problem to think of.
QEXPECT_FAIL( "", "Hostname changes are access-controlled", Continue );
QVERIFY( setSystemdHostname( "tubophone.calamares.io" ) );
}
void
UsersTests::cleanup()
{
if ( QTest::currentTestFailed() )
{
m_dir.setAutoRemove( false );
}
}
QTEST_GUILESS_MAIN( UsersTests )
#include "utils/moc-warnings.h"
#include "TestSetHostNameJob.moc"

View File

@ -1,6 +1,7 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2020, Adriaan de Groot <groot@kde.org>
* SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,129 +17,96 @@
* along with Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#include "SetHostNameJob.h"
#include "Config.h"
// Implementation details
extern bool setFileHostname( const QString& );
extern bool writeFileEtcHosts( const QString& );
extern bool setSystemdHostname( const QString& );
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include "utils/Yaml.h"
#include <QTemporaryDir>
#include <QtTest/QtTest>
class UsersTests : public QObject
// Implementation details
extern void setConfigurationDefaultGroups( const QVariantMap& map, QStringList& defaultGroups );
/** @brief Test Config object methods and internals
*
*/
class UserTests : public QObject
{
Q_OBJECT
public:
UsersTests();
virtual ~UsersTests() { }
UserTests();
virtual ~UserTests() {}
private Q_SLOTS:
void initTestCase();
void testEtcHostname();
void testEtcHosts();
void testHostnamed();
void cleanup();
private:
QTemporaryDir m_dir;
void testDefaultGroups();
};
UsersTests::UsersTests()
: m_dir( QStringLiteral( "/tmp/calamares-usertest" ) )
{
}
UserTests::UserTests() {}
void
UsersTests::initTestCase()
UserTests::initTestCase()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
cDebug() << "Users test started.";
cDebug() << "Test dir" << m_dir.path();
}
// Ensure we have a system object, expect it to be a "bogus" one
CalamaresUtils::System* system = CalamaresUtils::System::instance();
QVERIFY( system );
QVERIFY( system->doChroot() );
// Ensure we have a system-wide GlobalStorage with /tmp as root
if ( !Calamares::JobQueue::instance() )
void
UserTests::testDefaultGroups()
{
{
cDebug() << "Creating new JobQueue";
(void)new Calamares::JobQueue();
QStringList groups;
QVariantMap hweelGroup;
QVERIFY( groups.isEmpty() );
hweelGroup.insert( "defaultGroups", QStringList { "hweel" } );
setConfigurationDefaultGroups( hweelGroup, groups );
QCOMPARE( groups.count(), 1 );
QVERIFY( groups.contains( "hweel" ) );
}
Calamares::GlobalStorage* gs
= Calamares::JobQueue::instance() ? Calamares::JobQueue::instance()->globalStorage() : nullptr;
QVERIFY( gs );
gs->insert( "rootMountPoint", m_dir.path() );
}
void
UsersTests::testEtcHostname()
{
cDebug() << "Test dir" << m_dir.path();
QVERIFY( QFile::exists( m_dir.path() ) );
QVERIFY( !QFile::exists( m_dir.filePath( "etc" ) ) );
// Doesn't create intermediate directories
QVERIFY( !setFileHostname( QStringLiteral( "tubophone.calamares.io" ) ) );
QVERIFY( CalamaresUtils::System::instance()->createTargetDirs( "/etc" ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc" ) ) );
// Does write the file
QVERIFY( setFileHostname( QStringLiteral( "tubophone.calamares.io" ) ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc/hostname" ) ) );
// 22 for the test string, above, and 1 for the newline
QCOMPARE( QFileInfo( m_dir.filePath( "etc/hostname" ) ).size(), 22 + 1 );
}
void
UsersTests::testEtcHosts()
{
// Assume previous tests did their work
QVERIFY( QFile::exists( m_dir.path() ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc" ) ) );
QVERIFY( writeFileEtcHosts( QStringLiteral( "tubophone.calamares.io" ) ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc/hosts" ) ) );
// The skeleton contains %1 which has the hostname substituted in, so we lose two,
// and the rest of the blabla is 150 (according to Python)
QCOMPARE( QFileInfo( m_dir.filePath( "etc/hosts" ) ).size(), 150 + 22 - 2 );
}
void
UsersTests::testHostnamed()
{
// Since the service might not be running (e.g. non-systemd systems,
// FreeBSD, docker, ..) we're not going to fail a test here.
// There's also the permissions problem to think of.
QEXPECT_FAIL( "", "Hostname changes are access-controlled", Continue );
QVERIFY( setSystemdHostname( "tubophone.calamares.io" ) );
}
void
UsersTests::cleanup()
{
if ( QTest::currentTestFailed() )
{
m_dir.setAutoRemove( false );
QStringList desired { "wheel", "root", "operator" };
QStringList groups;
QVariantMap threeGroup;
QVERIFY( groups.isEmpty() );
threeGroup.insert( "defaultGroups", desired );
setConfigurationDefaultGroups( threeGroup, groups );
QCOMPARE( groups.count(), 3 );
QVERIFY( !groups.contains( "hweel" ) );
QCOMPARE( groups, desired );
}
{
QStringList groups;
QVariantMap explicitEmpty;
QVERIFY( groups.isEmpty() );
explicitEmpty.insert( "defaultGroups", QStringList() );
setConfigurationDefaultGroups( explicitEmpty, groups );
QCOMPARE( groups.count(), 0 );
}
{
QStringList groups;
QVariantMap missing;
QVERIFY( groups.isEmpty() );
setConfigurationDefaultGroups( missing, groups );
QCOMPARE( groups.count(), 6 ); // because of fallback!
QVERIFY( groups.contains( "lp" ) );
}
{
QStringList groups;
QVariantMap typeMismatch;
QVERIFY( groups.isEmpty() );
typeMismatch.insert( "defaultGroups", 1 );
setConfigurationDefaultGroups( typeMismatch, groups );
QCOMPARE( groups.count(), 6 ); // because of fallback!
QVERIFY( groups.contains( "lp" ) );
}
}
QTEST_GUILESS_MAIN( UsersTests )
QTEST_GUILESS_MAIN( UserTests )
#include "utils/moc-warnings.h"

View File

@ -25,16 +25,12 @@
#include "UsersPage.h"
#include "Config.h"
#include "ui_page_usersetup.h"
#include "CreateUserJob.h"
#include "SetHostNameJob.h"
#include "SetPasswordJob.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "Settings.h"
#include "utils/CalamaresUtilsGui.h"
#include "utils/Logger.h"
#include "utils/Retranslator.h"
@ -44,14 +40,6 @@
#include <QFile>
#include <QLabel>
#include <QLineEdit>
#include <QRegExp>
#include <QRegExpValidator>
static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" );
static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" );
static constexpr const int USERNAME_MAX_LENGTH = 31;
static constexpr const int HOSTNAME_MIN_LENGTH = 2;
static constexpr const int HOSTNAME_MAX_LENGTH = 63;
/** @brief How bad is the error for labelError() ? */
enum class Badness
@ -79,54 +67,87 @@ labelOk( QLabel* pix, QLabel* label )
pix->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, label->size() ) );
}
UsersPage::UsersPage( QWidget* parent )
/** Indicate error, update @p ok based on @p status */
static inline void
labelStatus( QLabel* pix, QLabel* label, const QString& value, const QString& status, bool& ok )
{
if ( status.isEmpty() )
{
if ( value.isEmpty() )
{
// This is different from labelOK() because no checkmark is shown
label->clear();
pix->clear();
ok = false;
}
else
{
labelOk( pix, label );
ok = true;
}
}
else
{
labelError( pix, label, status );
ok = false;
}
}
UsersPage::UsersPage( Config* config, QWidget* parent )
: QWidget( parent )
, ui( new Ui::Page_UserSetup )
, m_config( config )
, m_readyFullName( false )
, m_readyUsername( false )
, m_readyHostname( false )
, m_readyPassword( false )
, m_readyRootPassword( false )
, m_writeRootPassword( true )
{
ui->setupUi( this );
// Connect signals and slots
connect( ui->textBoxFullName, &QLineEdit::textEdited, this, &UsersPage::onFullNameTextEdited );
connect( ui->textBoxUsername, &QLineEdit::textEdited, this, &UsersPage::onUsernameTextEdited );
connect( ui->textBoxHostname, &QLineEdit::textEdited, this, &UsersPage::onHostnameTextEdited );
connect( ui->textBoxUserPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged );
connect( ui->textBoxUserVerifiedPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged );
connect( ui->textBoxRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged );
connect( ui->textBoxVerifiedRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged );
connect( ui->checkBoxValidatePassword, &QCheckBox::stateChanged, this, [ this ]( int ) {
connect( ui->checkBoxValidatePassword, &QCheckBox::stateChanged, this, [this]( int ) {
onPasswordTextChanged( ui->textBoxUserPassword->text() );
onRootPasswordTextChanged( ui->textBoxRootPassword->text() );
checkReady( isReady() );
} );
connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [ this ]( int checked ) {
connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [this]( const int checked ) {
/* When "reuse" is checked, hide the fields for explicitly
* entering the root password. However, if we're going to
* disable the root password anyway, hide them all regardless of
* the checkbox -- so when writeRoot is false, checked needs
* to be true, to hide them all.
*/
if ( !m_writeRootPassword )
{
checked = true;
}
ui->labelChooseRootPassword->setVisible( !checked );
ui->labelRootPassword->setVisible( !checked );
ui->labelRootPasswordError->setVisible( !checked );
ui->textBoxRootPassword->setVisible( !checked );
ui->textBoxVerifiedRootPassword->setVisible( !checked );
const bool visible = m_config->writeRootPassword() ? !checked : false;
ui->labelChooseRootPassword->setVisible( visible );
ui->labelRootPassword->setVisible( visible );
ui->labelRootPasswordError->setVisible( visible );
ui->textBoxRootPassword->setVisible( visible );
ui->textBoxVerifiedRootPassword->setVisible( visible );
checkReady( isReady() );
} );
m_customUsername = false;
m_customHostname = false;
connect( ui->textBoxFullName, &QLineEdit::textEdited, config, &Config::setFullName );
connect( config, &Config::fullNameChanged, this, &UsersPage::onFullNameTextEdited );
setWriteRootPassword( true );
connect( ui->textBoxHostName, &QLineEdit::textEdited, config, &Config::setHostName );
connect( config, &Config::hostNameChanged, ui->textBoxHostName, &QLineEdit::setText );
connect( config, &Config::hostNameStatusChanged, this, &UsersPage::reportHostNameStatus );
connect( ui->textBoxLoginName, &QLineEdit::textEdited, config, &Config::setLoginName );
connect( config, &Config::loginNameChanged, ui->textBoxLoginName, &QLineEdit::setText );
connect( config, &Config::loginNameStatusChanged, this, &UsersPage::reportLoginNameStatus );
connect( ui->checkBoxDoAutoLogin, &QCheckBox::stateChanged, this, [this]( int checked ) {
m_config->setAutoLogin( checked != Qt::Unchecked );
} );
connect( config, &Config::autoLoginChanged, ui->checkBoxDoAutoLogin, &QCheckBox::setChecked );
ui->checkBoxReusePassword->setVisible( m_config->writeRootPassword() );
ui->checkBoxReusePassword->setChecked( true );
ui->checkBoxValidatePassword->setChecked( true );
@ -146,15 +167,15 @@ UsersPage::retranslate()
ui->retranslateUi( this );
if ( Calamares::Settings::instance()->isSetupMode() )
{
ui->textBoxUsername->setToolTip( tr( "<small>If more than one person will "
"use this computer, you can create multiple "
"accounts after setup.</small>" ) );
ui->textBoxLoginName->setToolTip( tr( "<small>If more than one person will "
"use this computer, you can create multiple "
"accounts after setup.</small>" ) );
}
else
{
ui->textBoxUsername->setToolTip( tr( "<small>If more than one person will "
"use this computer, you can create multiple "
"accounts after installation.</small>" ) );
ui->textBoxLoginName->setToolTip( tr( "<small>If more than one person will "
"use this computer, you can create multiple "
"accounts after installation.</small>" ) );
}
// Re-do password checks (with output messages) as well.
// .. the password-checking methods get their values from the text boxes,
@ -165,27 +186,19 @@ UsersPage::retranslate()
bool
UsersPage::isReady()
UsersPage::isReady() const
{
bool readyFields = m_readyFullName && m_readyHostname && m_readyPassword && m_readyUsername;
if ( !m_writeRootPassword || ui->checkBoxReusePassword->isChecked() )
{
return readyFields;
}
return readyFields && m_readyRootPassword;
}
QString
UsersPage::getHostname() const
{
return ui->textBoxHostname->text();
// If we're going to write a root password, we need a valid one (or reuse the user's password)
readyFields
&= m_config->writeRootPassword() ? ( m_readyRootPassword || ui->checkBoxReusePassword->isChecked() ) : true;
return readyFields;
}
QString
UsersPage::getRootPassword() const
{
if ( m_writeRootPassword )
if ( m_config->writeRootPassword() )
{
if ( ui->checkBoxReusePassword->isChecked() )
{
@ -205,42 +218,24 @@ UsersPage::getRootPassword() const
QPair< QString, QString >
UsersPage::getUserPassword() const
{
return QPair< QString, QString >( ui->textBoxUsername->text(), ui->textBoxUserPassword->text() );
return QPair< QString, QString >( m_config->loginName(), ui->textBoxUserPassword->text() );
}
QList< Calamares::job_ptr >
UsersPage::createJobs( const QStringList& defaultGroupsList )
void
UsersPage::fillGlobalStorage() const
{
QList< Calamares::job_ptr > list;
if ( !isReady() )
{
return list;
return;
}
Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
Calamares::Job* j;
j = new CreateUserJob( ui->textBoxUsername->text(),
ui->textBoxFullName->text().isEmpty() ? ui->textBoxUsername->text()
: ui->textBoxFullName->text(),
ui->checkBoxAutoLogin->isChecked(),
defaultGroupsList );
list.append( Calamares::job_ptr( j ) );
if ( m_writeRootPassword )
if ( m_config->writeRootPassword() )
{
gs->insert( "reuseRootPassword", ui->checkBoxReusePassword->isChecked() );
}
gs->insert( "hostname", ui->textBoxHostname->text() );
if ( ui->checkBoxAutoLogin->isChecked() )
{
gs->insert( "autologinUser", ui->textBoxUsername->text() );
}
gs->insert( "username", ui->textBoxUsername->text() );
gs->insert( "password", CalamaresUtils::obscure( ui->textBoxUserPassword->text() ) );
return list;
}
@ -254,215 +249,23 @@ UsersPage::onActivate()
void
UsersPage::setWriteRootPassword( bool write )
UsersPage::onFullNameTextEdited( const QString& fullName )
{
m_writeRootPassword = write;
ui->checkBoxReusePassword->setVisible( write );
}
void
UsersPage::onFullNameTextEdited( const QString& textRef )
{
if ( textRef.isEmpty() )
{
ui->labelFullNameError->clear();
ui->labelFullName->clear();
if ( !m_customUsername )
{
ui->textBoxUsername->clear();
}
if ( !m_customHostname )
{
ui->textBoxHostname->clear();
}
m_readyFullName = false;
}
else
{
ui->labelFullName->setPixmap(
CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, ui->labelFullName->size() ) );
m_readyFullName = true;
fillSuggestions();
}
labelStatus( ui->labelFullName, ui->labelFullNameError, fullName, QString(), m_readyFullName );
checkReady( isReady() );
}
/** @brief Guess the machine's name
*
* If there is DMI data, use that; otherwise, just call the machine "-pc".
* Reads the DMI data just once.
*/
static QString
guessProductName()
{
static bool tried = false;
static QString dmiProduct;
if ( !tried )
{
// yes validateHostnameText() but these files can be a mess
QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive );
QFile dmiFile( QStringLiteral( "/sys/devices/virtual/dmi/id/product_name" ) );
if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) )
{
dmiProduct = QString::fromLocal8Bit( dmiFile.readAll().simplified().data() )
.toLower()
.replace( dmirx, " " )
.remove( ' ' );
}
if ( dmiProduct.isEmpty() )
{
dmiProduct = QStringLiteral( "-pc" );
}
tried = true;
}
return dmiProduct;
}
void
UsersPage::fillSuggestions()
UsersPage::reportLoginNameStatus( const QString& status )
{
QString fullName = ui->textBoxFullName->text();
QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive );
QString cleanName = CalamaresUtils::removeDiacritics( fullName ).toLower().replace( rx, " " ).simplified();
QStringList cleanParts = cleanName.split( ' ' );
if ( !m_customUsername )
{
if ( !cleanParts.isEmpty() && !cleanParts.first().isEmpty() )
{
QString usernameSuggestion = cleanParts.first();
for ( int i = 1; i < cleanParts.length(); ++i )
{
if ( !cleanParts.value( i ).isEmpty() )
{
usernameSuggestion.append( cleanParts.value( i ).at( 0 ) );
}
}
if ( USERNAME_RX.indexIn( usernameSuggestion ) != -1 )
{
ui->textBoxUsername->setText( usernameSuggestion );
validateUsernameText( usernameSuggestion );
m_customUsername = false;
}
}
}
if ( !m_customHostname )
{
if ( !cleanParts.isEmpty() && !cleanParts.first().isEmpty() )
{
QString hostnameSuggestion;
QString productName = guessProductName();
hostnameSuggestion = QString( "%1-%2" ).arg( cleanParts.first() ).arg( productName );
if ( HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 )
{
ui->textBoxHostname->setText( hostnameSuggestion );
validateHostnameText( hostnameSuggestion );
m_customHostname = false;
}
}
}
}
void
UsersPage::onUsernameTextEdited( const QString& textRef )
{
m_customUsername = true;
validateUsernameText( textRef );
}
void
UsersPage::validateUsernameText( const QString& textRef )
{
QString text( textRef );
QRegExpValidator val_whole( USERNAME_RX );
QRegExpValidator val_start( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator
int pos = -1;
if ( text.isEmpty() )
{
ui->labelUsernameError->clear();
ui->labelUsername->clear();
m_readyUsername = false;
}
else if ( text.length() > USERNAME_MAX_LENGTH )
{
labelError( ui->labelUsername, ui->labelUsernameError, tr( "Your username is too long." ) );
m_readyUsername = false;
}
else if ( val_start.validate( text, pos ) == QValidator::Invalid )
{
labelError( ui->labelUsername,
ui->labelUsernameError,
tr( "Your username must start with a lowercase letter or underscore." ) );
m_readyUsername = false;
}
else if ( val_whole.validate( text, pos ) == QValidator::Invalid )
{
labelError( ui->labelUsername,
ui->labelUsernameError,
tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ) );
m_readyUsername = false;
}
else
{
labelOk( ui->labelUsername, ui->labelUsernameError );
m_readyUsername = true;
}
labelStatus( ui->labelUsername, ui->labelUsernameError, m_config->loginName(), status, m_readyUsername );
emit checkReady( isReady() );
}
void
UsersPage::onHostnameTextEdited( const QString& textRef )
UsersPage::reportHostNameStatus( const QString& status )
{
m_customHostname = true;
validateHostnameText( textRef );
}
void
UsersPage::validateHostnameText( const QString& textRef )
{
QString text = textRef;
QRegExpValidator val( HOSTNAME_RX );
int pos = -1;
if ( text.isEmpty() )
{
ui->labelHostnameError->clear();
ui->labelHostname->clear();
m_readyHostname = false;
}
else if ( text.length() < HOSTNAME_MIN_LENGTH )
{
labelError( ui->labelHostname, ui->labelHostnameError, tr( "Your hostname is too short." ) );
m_readyHostname = false;
}
else if ( text.length() > HOSTNAME_MAX_LENGTH )
{
labelError( ui->labelHostname, ui->labelHostnameError, tr( "Your hostname is too long." ) );
m_readyHostname = false;
}
else if ( val.validate( text, pos ) == QValidator::Invalid )
{
labelError( ui->labelHostname,
ui->labelHostnameError,
tr( "Only letters, numbers, underscore and hyphen are allowed." ) );
m_readyHostname = false;
}
else
{
labelOk( ui->labelHostname, ui->labelHostnameError );
m_readyHostname = true;
}
labelStatus( ui->labelHostname, ui->labelHostnameError, m_config->hostName(), status, m_readyHostname );
emit checkReady( isReady() );
}
@ -546,13 +349,6 @@ UsersPage::setValidatePasswordDefault( bool checked )
emit checkReady( isReady() );
}
void
UsersPage::setAutologinDefault( bool checked )
{
ui->checkBoxAutoLogin->setChecked( checked );
emit checkReady( isReady() );
}
void
UsersPage::setReusePasswordDefault( bool checked )
{

View File

@ -25,10 +25,11 @@
#define USERSPAGE_H
#include "CheckPWQuality.h"
#include "Job.h"
#include <QWidget>
class Config;
class QLabel;
namespace Ui
@ -40,19 +41,17 @@ class UsersPage : public QWidget
{
Q_OBJECT
public:
explicit UsersPage( QWidget* parent = nullptr );
explicit UsersPage( Config* config, QWidget* parent = nullptr );
virtual ~UsersPage();
bool isReady();
bool isReady() const;
Calamares::JobList createJobs( const QStringList& defaultGroupsList );
void fillGlobalStorage() const;
void onActivate();
void setWriteRootPassword( bool show );
void setPasswordCheckboxVisible( bool visible );
void setValidatePasswordDefault( bool checked );
void setAutologinDefault( bool checked );
void setReusePasswordDefault( bool checked );
/** @brief Process entries in the passwordRequirements config entry
@ -63,8 +62,6 @@ public:
*/
void addPasswordCheck( const QString& key, const QVariant& value );
///@brief Hostname as entered / auto-filled
QString getHostname() const;
///@brief Root password, depends on settings, may be empty
QString getRootPassword() const;
///@brief User name and password
@ -72,11 +69,8 @@ public:
protected slots:
void onFullNameTextEdited( const QString& );
void fillSuggestions();
void onUsernameTextEdited( const QString& );
void validateUsernameText( const QString& );
void onHostnameTextEdited( const QString& );
void validateHostnameText( const QString& );
void reportLoginNameStatus( const QString& );
void reportHostNameStatus( const QString& );
void onPasswordTextChanged( const QString& );
void onRootPasswordTextChanged( const QString& );
@ -95,19 +89,16 @@ private:
void retranslate();
Ui::Page_UserSetup* ui;
Config* m_config;
PasswordCheckList m_passwordChecks;
bool m_passwordChecksChanged = false;
bool m_readyFullName;
bool m_readyUsername;
bool m_customUsername;
bool m_readyHostname;
bool m_customHostname;
bool m_readyPassword;
bool m_readyRootPassword;
bool m_writeRootPassword;
};
#endif // USERSPAGE_H

View File

@ -20,17 +20,18 @@
#include "UsersViewStep.h"
#include "Config.h"
#include "CreateUserJob.h"
#include "SetHostNameJob.h"
#include "SetPasswordJob.h"
#include "UsersPage.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/Logger.h"
#include "utils/NamedEnum.h"
#include "utils/Variant.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
CALAMARES_PLUGIN_FACTORY_DEFINITION( UsersViewStepFactory, registerPlugin< UsersViewStep >(); )
static const NamedEnumTable< SetHostNameJob::Action >&
@ -53,11 +54,11 @@ hostnameActions()
UsersViewStep::UsersViewStep( QObject* parent )
: Calamares::ViewStep( parent )
, m_widget( new UsersPage() )
, m_widget( nullptr )
, m_actions( SetHostNameJob::Action::None )
, m_config( new Config( this ) )
{
emit nextStatusChanged( true );
connect( m_widget, &UsersPage::checkReady, this, &UsersViewStep::nextStatusChanged );
}
@ -80,6 +81,11 @@ UsersViewStep::prettyName() const
QWidget*
UsersViewStep::widget()
{
if ( !m_widget )
{
m_widget = new UsersPage( m_config );
connect( m_widget, &UsersPage::checkReady, this, &UsersViewStep::nextStatusChanged );
}
return m_widget;
}
@ -87,7 +93,7 @@ UsersViewStep::widget()
bool
UsersViewStep::isNextEnabled() const
{
return m_widget->isReady();
return m_widget ? m_widget->isReady() : true;
}
@ -122,7 +128,10 @@ UsersViewStep::jobs() const
void
UsersViewStep::onActivate()
{
m_widget->onActivate();
if ( m_widget )
{
m_widget->onActivate();
}
}
@ -130,9 +139,17 @@ void
UsersViewStep::onLeave()
{
m_jobs.clear();
m_jobs.append( m_widget->createJobs( m_defaultGroups ) );
if ( !m_widget || !m_widget->isReady() )
{
return;
}
Calamares::Job* j;
// TODO: Config object should create jobs, like this one, that depend only on config values
j = new CreateUserJob( m_config->loginName(),
m_config->fullName().isEmpty() ? m_config->loginName() : m_config->fullName(),
m_config->doAutoLogin(),
m_config->defaultGroups() );
auto userPW = m_widget->getUserPassword();
j = new SetPasswordJob( userPW.first, userPW.second );
@ -141,46 +158,20 @@ UsersViewStep::onLeave()
j = new SetPasswordJob( "root", m_widget->getRootPassword() );
m_jobs.append( Calamares::job_ptr( j ) );
j = new SetHostNameJob( m_widget->getHostname(), m_actions );
j = new SetHostNameJob( m_config->hostName(), m_actions );
m_jobs.append( Calamares::job_ptr( j ) );
m_widget->fillGlobalStorage();
}
void
UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap )
{
// Create the widget, after all .. as long as writing configuration to the UI is needed
(void)this->widget();
using CalamaresUtils::getBool;
if ( configurationMap.contains( "defaultGroups" )
&& configurationMap.value( "defaultGroups" ).type() == QVariant::List )
{
m_defaultGroups = configurationMap.value( "defaultGroups" ).toStringList();
}
else
{
cWarning() << "Using fallback groups. Please check defaultGroups in users.conf";
m_defaultGroups = QStringList { "lp", "video", "network", "storage", "wheel", "audio" };
}
if ( configurationMap.contains( "autologinGroup" )
&& configurationMap.value( "autologinGroup" ).type() == QVariant::String )
{
Calamares::JobQueue::instance()->globalStorage()->insert(
"autologinGroup", configurationMap.value( "autologinGroup" ).toString() );
}
if ( configurationMap.contains( "sudoersGroup" )
&& configurationMap.value( "sudoersGroup" ).type() == QVariant::String )
{
Calamares::JobQueue::instance()->globalStorage()->insert( "sudoersGroup",
configurationMap.value( "sudoersGroup" ).toString() );
}
bool setRootPassword = getBool( configurationMap, "setRootPassword", true );
Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", setRootPassword );
m_widget->setWriteRootPassword( setRootPassword );
m_widget->setAutologinDefault( getBool( configurationMap, "doAutologin", false ) );
m_widget->setReusePasswordDefault( getBool( configurationMap, "doReusePassword", false ) );
if ( configurationMap.contains( "passwordRequirements" )
@ -197,15 +188,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap )
m_widget->setPasswordCheckboxVisible( getBool( configurationMap, "allowWeakPasswords", false ) );
m_widget->setValidatePasswordDefault( !getBool( configurationMap, "allowWeakPasswordsDefault", false ) );
QString shell( QLatin1String( "/bin/bash" ) ); // as if it's not set at all
if ( configurationMap.contains( "userShell" ) )
{
shell = CalamaresUtils::getString( configurationMap, "userShell" );
}
// Now it might be explicitly set to empty, which is ok
Calamares::JobQueue::instance()->globalStorage()->insert( "userShell", shell );
using Action = SetHostNameJob::Action;
QString hostnameActionString = CalamaresUtils::getString( configurationMap, "setHostname" );
@ -222,4 +204,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap )
Action hostsfileAction = getBool( configurationMap, "writeHostsFile", true ) ? Action::WriteEtcHosts : Action::None;
m_actions = hostsfileAction | hostnameAction;
m_config->setConfigurationMap( configurationMap );
}

View File

@ -29,6 +29,7 @@
#include <QObject>
#include <QVariant>
class Config;
class UsersPage;
class PLUGINDLLEXPORT UsersViewStep : public Calamares::ViewStep
@ -60,8 +61,9 @@ private:
UsersPage* m_widget;
QList< Calamares::job_ptr > m_jobs;
QStringList m_defaultGroups;
SetHostNameJob::Actions m_actions;
Config* m_config;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( UsersViewStepFactory )

View File

@ -127,7 +127,7 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="textBoxUsername">
<widget class="QLineEdit" name="textBoxLoginName">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -226,7 +226,7 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLineEdit" name="textBoxHostname">
<widget class="QLineEdit" name="textBoxHostName">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -456,7 +456,7 @@
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBoxAutoLogin">
<widget class="QCheckBox" name="checkBoxDoAutoLogin">
<property name="text">
<string>Log in automatically without asking for the password.</string>
</property>