diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..c2699507d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +name: ci + +on: + push: + branches: + - calamares + pull_request: + types: + - opened + - reopened + - synchronize + workflow_dispatch: + +env: + BUILDDIR: /build + SRCDIR: ${{ github.workspace }} + CMAKE_ARGS: | + -DWEBVIEW_FORCE_WEBKIT=1 + -DKDE_INSTALL_USE_QT_SYS_PATHS=ON + -DWITH_PYTHONQT=OFF" + -DCMAKE_BUILD_TYPE=Debug + +jobs: + build: + runs-on: ubuntu-latest + container: + image: docker://kdeneon/plasma:user + options: --tmpfs /build:rw --user 0:0 + steps: + - + name: prepare env + run: | + sudo apt-get update + sudo apt-get -y install git-core + - + name: checkout + uses: actions/checkout@v2 + - + name: install dependencies + run: | + sudo apt-get -y install \ + build-essential \ + cmake \ + extra-cmake-modules \ + gettext \ + kio-dev \ + libatasmart-dev \ + libboost-python-dev \ + libkf5config-dev \ + libkf5coreaddons-dev \ + libkf5i18n-dev \ + libkf5iconthemes-dev \ + libkf5parts-dev \ + libkf5service-dev \ + libkf5solid-dev \ + libkpmcore-dev \ + libparted-dev \ + libpolkit-qt5-1-dev \ + libqt5svg5-dev \ + libqt5webkit5-dev \ + libyaml-cpp-dev \ + os-prober \ + pkg-config \ + python3-dev \ + qtbase5-dev \ + qtdeclarative5-dev \ + qttools5-dev \ + qttools5-dev-tools + - + name: prepare build + run: | + test -n "$BUILDDIR" || { echo "! \$BUILDDIR not set" ; exit 1 ; } + mkdir -p $BUILDDIR + test -f $SRCDIR/CMakeLists.txt || { echo "! Missing $SRCDIR/CMakeLists.txt" ; exit 1 ; } + - + name: cmake + working-directory: ${{ env.BUILDDIR }} + run: cmake $CMAKE_ARGS $SRCDIR + - + name: make + working-directory: ${{ env.BUILDDIR }} + run: make -j2 VERBOSE=1 + - + name: install + working-directory: ${{ env.BUILDDIR }} + run: | + make install VERBOSE=1 + - + name: notify + uses: rectalogic/notify-irc@v1 + with: + server: chat.freenode.net + channel: "#calamares" + nickname: gh-notify + message: | + ${{ github.actor }} pushed ${{ github.event.ref }} CI ${{ steps.install.conclusion }} JOB ${{ github.job }} RUN ${{ github.run_id }} diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 000000000..c647936d7 --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,17 @@ +name: issues + +on: issues + +jobs: + irc: + runs-on: ubuntu-latest + steps: + - + name: notify + uses: rectalogic/notify-irc@v1 + with: + server: chat.freenode.net + channel: "#calamares" + nickname: gh-issues + message: | + ${{ github.actor }} issue ${{ github.event.issue.title }} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7a1cad92a..000000000 --- a/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-FileCopyrightText: 2017 Rohan Garg -# SPDX-License-Identifier: BSD-2-Clause - -FROM kdeneon/all:user -RUN sudo apt-get update && sudo apt-get -y install build-essential cmake extra-cmake-modules gettext kio-dev libatasmart-dev libboost-python-dev libkf5config-dev libkf5coreaddons-dev libkf5i18n-dev libkf5iconthemes-dev libkf5parts-dev libkf5service-dev libkf5solid-dev libkpmcore-dev libparted-dev libpolkit-qt5-1-dev libqt5svg5-dev libqt5webkit5-dev libyaml-cpp-dev os-prober pkg-config python3-dev qtbase5-dev qtdeclarative5-dev qttools5-dev qttools5-dev-tools diff --git a/README.md b/README.md index d3db39089..21c393e29 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ --------- [![GitHub release](https://img.shields.io/github/release/calamares/calamares.svg)](https://github.com/calamares/calamares/releases) +[![GitHub Build Status](https://img.shields.io/github/workflow/status/calamares/calamares/ci?label=GH%20build)](https://github.com/calamares/calamares/actions?query=workflow%3Aci) [![Travis Build Status](https://travis-ci.org/calamares/calamares.svg?branch=calamares)](https://travis-ci.org/calamares/calamares) [![Coverity Scan Build Status](https://scan.coverity.com/projects/5389/badge.svg)](https://scan.coverity.com/projects/5389) [![GitHub license](https://img.shields.io/github/license/calamares/calamares.svg)](https://github.com/calamares/calamares/blob/calamares/LICENSE) diff --git a/ci/travis-config.sh b/ci/travis-config.sh deleted file mode 100644 index 15163cc99..000000000 --- a/ci/travis-config.sh +++ /dev/null @@ -1,16 +0,0 @@ -# Build configuration on Travis. -# -# SPDX-FileCopyrightText: 2018 Adriaan de Groot -# SPDX-License-Identifier: BSD-2-Clause -# -# Defines a CMAKE_ARGS variable for use with cmake -# -# This file is sourced by travis.sh, and exports the variables -# to the environment. -CMAKE_ARGS="\ - -DCMAKE_BUILD_TYPE=Release \ - -DWEBVIEW_FORCE_WEBKIT=1 \ - -DKDE_INSTALL_USE_QT_SYS_PATHS=ON \ - -DWITH_PYTHONQT=OFF" - -export CMAKE_ARGS diff --git a/ci/travis-continuous.sh b/ci/travis-continuous.sh deleted file mode 100755 index ceb80df9b..000000000 --- a/ci/travis-continuous.sh +++ /dev/null @@ -1,58 +0,0 @@ -#! /bin/sh -# -# SPDX-FileCopyrightText: 2017 Adriaan de Groot -# SPDX-License-Identifier: BSD-2-Clause -# -# Travis CI script for use on every-commit: -# - build and install Calamares -# -test -n "$BUILDDIR" || { echo "! \$BUILDDIR not set" ; exit 1 ; } -test -n "$SRCDIR" || { echo "! \$SRCDIR not set" ; exit 1 ; } - -test -d $BUILDDIR || { echo "! $BUILDDIR not a directory" ; exit 1 ; } -test -d $SRCDIR || { echo "! $SRCDIR not a directory" ; exit 1 ; } -test -f $SRCDIR/CMakeLists.txt || { echo "! Missing $SRCDIR/CMakeLists.txt" ; exit 1 ; } - -cd $BUILDDIR || exit 1 - -section() { -echo "###" -echo "### $1" -echo "###" -pwd -P -df -h -} - -section "cmake $CMAKE_ARGS $SRCDIR" -cmake $CMAKE_ARGS $SRCDIR || { echo "! CMake failed" ; exit 1 ; } - -section "make" -make -j2 VERBOSE=1 || { echo "! Make recheck" ; pwd -P ; df -h ; make -j1 VERBOSE=1 ; echo "! Make failed" ; exit 1 ; } - -section "make install" - -install_debugging() { - ls -la $( find "$1" -type f -name '*.so' ) -} - -echo "# Build results" -install_debugging "$BUILDDIR" - -echo "# Install" -DESTDIR=/build/INSTALL_ROOT -mkdir -p "$DESTDIR" - -if make install VERBOSE=1 DESTDIR="$DESTDIR" ; -then - echo "# .. install OK" - result=true -else - echo "# .. install failed" - result=false -fi - - -section "Install results" -install_debugging "$DESTDIR" - -$result || { echo "! Install failed" ; exit 1 ; } # Result of make install, above diff --git a/ci/travis-coverity.sh b/ci/travis-coverity.sh deleted file mode 100755 index 5ec73568a..000000000 --- a/ci/travis-coverity.sh +++ /dev/null @@ -1,37 +0,0 @@ -#! /bin/sh -# -# SPDX-FileCopyrightText: 2017 Adriaan de Groot -# SPDX-License-Identifier: BSD-2-Clause -# -# Travis CI script for weekly (cron) use: -# - use the coverity tool to build and and upload results -# -test -n "$COVERITY_SCAN_TOKEN" || { echo "! Missing Coverity token" ; exit 1 ; } -test -n "$BUILDDIR" || { echo "! \$BUILDDIR not set" ; exit 1 ; } -test -n "$SRCDIR" || { echo "! \$SRCDIR not set" ; exit 1 ; } - -test -d $BUILDDIR || { echo "! $BUILDDIR not a directory" ; exit 1 ; } -test -d $SRCDIR || { echo "! $SRCDIR not a directory" ; exit 1 ; } -test -f $SRCDIR/CMakeLists.txt || { echo "! Missing $SRCDIR/CMakeLists.txt" ; exit 1 ; } - -cd $BUILDDIR || exit 1 - -curl -k -o coverity_tool.tar.gz \ - -d "token=$COVERITY_SCAN_TOKEN&project=calamares%2Fcalamares" \ - https://scan.coverity.com/download/cxx/linux64 || exit 1 -mkdir "$BUILDDIR/coveritytool" -tar xvf coverity_tool.tar.gz -C "$BUILDDIR/coveritytool" --strip-components 2 -export PATH="$BUILDDIR/coveritytool/bin:$PATH" - -echo "# cmake -DCMAKE_BUILD_TYPE=Debug $CMAKE_ARGS $SRCDIR" -cmake -DCMAKE_BUILD_TYPE=Debug $CMAKE_ARGS $SRCDIR || exit 1 -cov-build --dir cov-int make -j2 - -tar caf calamares-ci.tar.xz cov-int - -curl -k --form token=$COVERITY_SCAN_TOKEN \ - --form email=groot@kde.org \ - --form file=@calamares-ci.tar.xz \ - --form version="calamares-`date -u +%Y%m%d`" \ - --form description="calamares on `date -u`" \ - https://scan.coverity.com/builds?project=calamares%2Fcalamares diff --git a/ci/travis.sh b/ci/travis.sh deleted file mode 100755 index e182e48bb..000000000 --- a/ci/travis.sh +++ /dev/null @@ -1,24 +0,0 @@ -#! /bin/sh -# -# SPDX-FileCopyrightText: 2017 Adriaan de Groot -# SPDX-License-Identifier: BSD-2-Clause -# -# Travis build driver script: -# - the regular CI runs, triggered by commits, run a script that builds -# and installs calamares, and then runs the tests. -# - the cronjob CI runs, triggered weekly, run a script that uses the -# coverity tools to submit a build. This is slightly more resource- -# intensive than the coverity add-on, but works on master. -# -D=`dirname "$0"` -test -d "$D" || { echo "! No directory $D" ; exit 1 ; } -test -x "$D/travis-continuous.sh" || { echo "! Missing -continuous" ; exit 1 ; } -test -x "$D/travis-coverity.sh" || { echo "! Missing -coverity" ; exit 1 ; } - -test -f "$D/travis-config.sh" && . "$D/travis-config.sh" - -if test "$TRAVIS_EVENT_TYPE" = "cron" ; then - exec "$D/travis-coverity.sh" -else - exec "$D/travis-continuous.sh" -fi diff --git a/src/libcalamares/PythonJob.cpp b/src/libcalamares/PythonJob.cpp index 6944f38e5..98f284ecc 100644 --- a/src/libcalamares/PythonJob.cpp +++ b/src/libcalamares/PythonJob.cpp @@ -261,12 +261,12 @@ PythonJob::exec() { m_description.truncate( i_newline ); } - cDebug() << "Job description from __doc__" << prettyName() << '=' << m_description; + cDebug() << Logger::SubEntry << "Job description from __doc__" << prettyName() << '=' << m_description; } } else { - cDebug() << "Job description from pretty_name" << prettyName() << '=' << m_description; + cDebug() << Logger::SubEntry << "Job description from pretty_name" << prettyName() << '=' << m_description; } emit progress( 0 ); diff --git a/src/libcalamares/utils/CalamaresUtilsSystem.cpp b/src/libcalamares/utils/CalamaresUtilsSystem.cpp index 841d52969..29f743743 100644 --- a/src/libcalamares/utils/CalamaresUtilsSystem.cpp +++ b/src/libcalamares/utils/CalamaresUtilsSystem.cpp @@ -188,7 +188,7 @@ System::runCommand( System::RunLocation location, : -1 ) ) { cWarning() << "Process" << args.first() << "timed out after" << timeoutSec.count() << "s. Output so far:\n" - << Logger::NoQuote {} << process.readAllStandardOutput(); + << Logger::NoQuote << process.readAllStandardOutput(); return ProcessResult::Code::TimedOut; } @@ -196,7 +196,7 @@ System::runCommand( System::RunLocation location, if ( process.exitStatus() == QProcess::CrashExit ) { - cWarning() << "Process" << args.first() << "crashed. Output so far:\n" << Logger::NoQuote {} << output; + cWarning() << "Process" << args.first() << "crashed. Output so far:\n" << Logger::NoQuote << output; return ProcessResult::Code::Crashed; } @@ -206,7 +206,7 @@ System::runCommand( System::RunLocation location, { if ( showDebug && !output.isEmpty() ) { - cDebug() << Logger::SubEntry << "Finished. Exit code:" << r << "output:\n" << Logger::NoQuote {} << output; + cDebug() << Logger::SubEntry << "Finished. Exit code:" << r << "output:\n" << Logger::NoQuote << output; } else { @@ -218,7 +218,7 @@ System::runCommand( System::RunLocation location, if ( !output.isEmpty() ) { cDebug() << Logger::SubEntry << "Target cmd:" << RedactedList( args ) << "Exit code:" << r << "output:\n" - << Logger::NoQuote {} << output; + << Logger::NoQuote << output; } else { diff --git a/src/libcalamares/utils/Logger.cpp b/src/libcalamares/utils/Logger.cpp index 0a7dcefd0..262ff59e1 100644 --- a/src/libcalamares/utils/Logger.cpp +++ b/src/libcalamares/utils/Logger.cpp @@ -207,6 +207,8 @@ constexpr FuncSuppressor::FuncSuppressor( const char s[] ) const constexpr FuncSuppressor Continuation( s_Continuation ); const constexpr FuncSuppressor SubEntry( s_SubEntry ); +const constexpr NoQuote_t NoQuote {}; +const constexpr Quote_t Quote {}; QString toString( const QVariant& v ) diff --git a/src/libcalamares/utils/Logger.h b/src/libcalamares/utils/Logger.h index 58603c82d..a53ab7e19 100644 --- a/src/libcalamares/utils/Logger.h +++ b/src/libcalamares/utils/Logger.h @@ -25,15 +25,17 @@ struct FuncSuppressor const char* m_s; }; -struct NoQuote +struct NoQuote_t { }; -struct Quote +struct Quote_t { }; DLLEXPORT extern const FuncSuppressor Continuation; DLLEXPORT extern const FuncSuppressor SubEntry; +DLLEXPORT extern const NoQuote_t NoQuote; +DLLEXPORT extern const Quote_t Quote; enum { @@ -74,13 +76,13 @@ operator<<( QDebug& s, const FuncSuppressor& f ) } inline QDebug& -operator<<( QDebug& s, const NoQuote& ) +operator<<( QDebug& s, const NoQuote_t& ) { return s.noquote().nospace(); } inline QDebug& -operator<<( QDebug& s, const Quote& ) +operator<<( QDebug& s, const Quote_t& ) { return s.quote().space(); } @@ -254,7 +256,7 @@ operator<<( QDebug& s, const DebugMap& t ) inline QDebug& operator<<( QDebug& s, const Pointer& p ) { - s << NoQuote {} << '@' << p.ptr << Quote {}; + s << NoQuote << '@' << p.ptr << Quote; return s; } } // namespace Logger diff --git a/src/libcalamares/utils/String.cpp b/src/libcalamares/utils/String.cpp index 34a7038e3..0c7bf8fb5 100644 --- a/src/libcalamares/utils/String.cpp +++ b/src/libcalamares/utils/String.cpp @@ -15,6 +15,7 @@ */ #include "String.h" +#include "Logger.h" #include @@ -121,4 +122,61 @@ obscure( const QString& string ) return result; } + +QString +truncateMultiLine( const QString& string, CalamaresUtils::LinesStartEnd lines, CalamaresUtils::CharCount chars ) +{ + const char NEWLINE = '\n'; + const int maxLines = lines.atStart + lines.atEnd; + if ( maxLines < 1 ) + { + QString shorter( string ); + shorter.truncate( chars.total ); + return shorter; + } + + const int linesInString = string.count( NEWLINE ) + ( string.endsWith( NEWLINE ) ? 0 : 1 ); + if ( ( string.length() <= chars.total ) && ( linesInString <= maxLines ) ) + { + return string; + } + + QString front, back; + if ( string.count( NEWLINE ) >= maxLines ) + { + int from = -1; + for ( int i = 0; i < lines.atStart; ++i ) + { + from = string.indexOf( NEWLINE, from + 1 ); + if ( from < 0 ) + { + // That's strange, we counted at least maxLines newlines before + break; + } + } + if ( from > 0 ) + { + front = string.left( from + 1 ); + } + + int lastNewLine = -1; + int lastCount = string.endsWith( NEWLINE ) ? -1 : 0; + for ( auto i = string.rbegin(); i != string.rend() && lastCount < lines.atEnd; ++i ) + { + if ( *i == NEWLINE ) + { + ++lastCount; + lastNewLine = int( i - string.rbegin() ); + } + } + if ( ( lastNewLine >= 0 ) && ( lastCount >= lines.atEnd ) ) + { + back = string.right( lastNewLine ); + } + } + + return front + back; +} + + } // namespace CalamaresUtils diff --git a/src/libcalamares/utils/String.h b/src/libcalamares/utils/String.h index 48bb17aac..43e0474fa 100644 --- a/src/libcalamares/utils/String.h +++ b/src/libcalamares/utils/String.h @@ -61,6 +61,43 @@ DLLEXPORT QString removeDiacritics( const QString& string ); * @return the obfuscated string. */ DLLEXPORT QString obscure( const QString& string ); + +/** @brief Parameter for counting lines at beginning and end of string + * + * This is used by truncateMultiLine() to indicate how many lines from + * the beginning and how many from the end should be kept. + */ +struct LinesStartEnd +{ + int atStart = 0; + int atEnd = 0; +}; + +/** @brief Parameter for counting characters in truncateMultiLine() + */ +struct CharCount +{ + int total = 0; +}; + +/** @brief Truncate a string to some reasonable length for display + * + * Keep the first few, or last few (or both) lines of a possibly lengthy + * message @p string and reduce it to a displayable size (e.g. for + * pop-up windows that display the message). If the message is longer + * than @p chars, then characters are removed from the front (if + * @p lines.atStart is zero) or end (if @p lines.atEnd is zero) or in the middle + * (if both are nonzero). + * + * @param string the input string. + * @param lines number of lines to preserve. + * @param chars maximum number of characters in the returned string. + * @return a string built from parts of the input string. + */ +DLLEXPORT QString truncateMultiLine( const QString& string, + LinesStartEnd lines = LinesStartEnd { 3, 5 }, + CharCount chars = CharCount { 812 } ); + } // namespace CalamaresUtils #endif diff --git a/src/libcalamares/utils/Tests.cpp b/src/libcalamares/utils/Tests.cpp index 6d4f5b265..3992fe78a 100644 --- a/src/libcalamares/utils/Tests.cpp +++ b/src/libcalamares/utils/Tests.cpp @@ -13,6 +13,7 @@ #include "Entropy.h" #include "Logger.h" #include "RAII.h" +#include "String.h" #include "Traits.h" #include "UMask.h" #include "Variant.h" @@ -63,6 +64,9 @@ private Q_SLOTS: void testVariantStringListYAMLDashed(); void testVariantStringListYAMLBracketed(); + /** @brief Test smart string truncation. */ + void testStringTruncation(); + void testStringTruncationShorter(); private: void recursiveCompareMap( const QVariantMap& a, const QVariantMap& b, int depth ); @@ -495,6 +499,141 @@ strings: [ aap, noot, mies ] QVERIFY( !getStringList( m, key ).contains( "lam" ) ); } +void +LibCalamaresTests::testStringTruncation() +{ + Logger::setupLogLevel( Logger::LOGDEBUG ); + + using namespace CalamaresUtils; + + const QString longString( R"(--- +--- src/libcalamares/utils/String.h ++++ src/libcalamares/utils/String.h +@@ -62,15 +62,22 @@ DLLEXPORT QString removeDiacritics( const QString& string ); + */ + DLLEXPORT QString obscure( const QString& string ); + ++/** @brief Parameter for counting lines at beginning and end of string ++ * ++ * This is used by truncateMultiLine() to indicate how many lines from ++ * the beginning and how many from the end should be kept. ++ */ + struct LinesStartEnd + { +- int atStart; +- int atEnd; ++ int atStart = 0; ++ int atEnd = 0; +)" ); + + const int sufficientLength = 812; + // There's 18 lines in all + QCOMPARE( longString.count( '\n' ), 18 ); + QVERIFY( longString.length() < sufficientLength ); + + // If we ask for more, we get everything back + QCOMPARE( longString, truncateMultiLine( longString, LinesStartEnd { 20, 0 }, CharCount { sufficientLength } ) ); + QCOMPARE( longString, truncateMultiLine( longString, LinesStartEnd { 0, 20 }, CharCount { sufficientLength } ) ); + + // If we ask for no lines, only characters, we get that + { + auto s = truncateMultiLine( longString, LinesStartEnd { 0, 0 }, CharCount { 4 } ); + QCOMPARE( s.length(), 4 ); + QCOMPARE( s, QString( "---\n" ) ); + } + { + auto s = truncateMultiLine( longString, LinesStartEnd { 0, 0 }, CharCount { sufficientLength } ); + QCOMPARE( s, longString ); + } + + // Lines at the start + { + auto s = truncateMultiLine( longString, LinesStartEnd { 4, 0 }, CharCount { sufficientLength } ); + QVERIFY( s.length() > 1 ); + QVERIFY( longString.startsWith( s ) ); + cDebug() << "Result-line" << Logger::Quote << s; + QCOMPARE( s.count( '\n' ), 4 ); + } + + // Lines at the end + { + auto s = truncateMultiLine( longString, LinesStartEnd { 0, 4 }, CharCount { sufficientLength } ); + QVERIFY( s.length() > 1 ); + QVERIFY( longString.endsWith( s ) ); + cDebug() << "Result-line" << Logger::Quote << s; + QCOMPARE( s.count( '\n' ), 4 ); + } + + // Lines at both ends + { + auto s = truncateMultiLine( longString, LinesStartEnd { 2, 2 }, CharCount { sufficientLength } ); + QVERIFY( s.length() > 1 ); + cDebug() << "Result-line" << Logger::Quote << s; + QCOMPARE( s.count( '\n' ), 4 ); + + auto firsttwo = truncateMultiLine( s, LinesStartEnd { 2, 0 }, CharCount { sufficientLength } ); + auto lasttwo = truncateMultiLine( s, LinesStartEnd { 0, 2 }, CharCount { sufficientLength } ); + QCOMPARE( firsttwo + lasttwo, s ); + QCOMPARE( firsttwo.count( '\n' ), 2 ); + QVERIFY( longString.startsWith( firsttwo ) ); + QVERIFY( longString.endsWith( lasttwo ) ); + } +} + +void +LibCalamaresTests::testStringTruncationShorter() +{ + Logger::setupLogLevel( Logger::LOGDEBUG ); + + using namespace CalamaresUtils; + + const QString longString( R"(Some strange string artifacts appeared, leading to `{1?}` being +displayed in various user-facing messages. These have been removed +and the translations updated.)" ); + const char NEWLINE = '\n'; + + const int insufficientLength = 42; + // There's 2 newlines in all, no trailing newline + QVERIFY( !longString.endsWith( NEWLINE ) ); + QCOMPARE( longString.count( NEWLINE ), 2 ); + QVERIFY( longString.length() > insufficientLength ); + + // Grab first line, untruncated + { + auto s = truncateMultiLine( longString, LinesStartEnd { 1, 0 } ); + QVERIFY( s.length() > 1 ); + QVERIFY( longString.startsWith( s ) ); + QVERIFY( s.endsWith( NEWLINE ) ); + QVERIFY( s.endsWith( "being\n" ) ); + QVERIFY( s.startsWith( "Some " ) ); + } + + // Grab last line, untruncated + { + auto s = truncateMultiLine( longString, LinesStartEnd { 0, 1 } ); + QVERIFY( s.length() > 1 ); + QVERIFY( longString.endsWith( s ) ); + QVERIFY( !s.endsWith( NEWLINE ) ); + QVERIFY( s.endsWith( "updated." ) ); + QCOMPARE( s.count( NEWLINE ), 0 ); // Because last line doesn't end with a newline + QVERIFY( s.startsWith( "and the " ) ); + } + + // Grab last two lines, untruncated + { + auto s = truncateMultiLine( longString, LinesStartEnd { 0, 2 } ); + QVERIFY( s.length() > 1 ); + QVERIFY( longString.endsWith( s ) ); + QVERIFY( !s.endsWith( NEWLINE ) ); + QVERIFY( s.endsWith( "updated." ) ); + cDebug() << "Result-line" << Logger::Quote << s; + QCOMPARE( s.count( NEWLINE ), 1 ); // Because last line doesn't end with a newline + QVERIFY( s.startsWith( "displayed in " ) ); + } + +} + + QTEST_GUILESS_MAIN( LibCalamaresTests ) #include "utils/moc-warnings.h" diff --git a/src/libcalamaresui/Branding.cpp b/src/libcalamaresui/Branding.cpp index 8145ad57c..a5038d7ee 100644 --- a/src/libcalamaresui/Branding.cpp +++ b/src/libcalamaresui/Branding.cpp @@ -35,7 +35,7 @@ [[noreturn]] static void bail( const QString& descriptorPath, const QString& message ) { - cError() << "FATAL in" << descriptorPath << Logger::Continuation << Logger::NoQuote {} << message; + cError() << "FATAL in" << descriptorPath << Logger::Continuation << Logger::NoQuote << message; ::exit( EXIT_FAILURE ); } diff --git a/src/libcalamaresui/ViewManager.cpp b/src/libcalamaresui/ViewManager.cpp index f43152209..39f0bb902 100644 --- a/src/libcalamaresui/ViewManager.cpp +++ b/src/libcalamaresui/ViewManager.cpp @@ -19,6 +19,7 @@ #include "utils/Logger.h" #include "utils/Paste.h" #include "utils/Retranslator.h" +#include "utils/String.h" #include "viewpages/BlankViewStep.h" #include "viewpages/ExecutionViewStep.h" #include "viewpages/ViewStep.h" @@ -30,10 +31,11 @@ #include #define UPDATE_BUTTON_PROPERTY( name, value ) \ + do \ { \ m_##name = value; \ emit name##Changed( m_##name ); \ - } + } while ( false ) namespace Calamares { @@ -136,15 +138,14 @@ ViewManager::insertViewStep( int before, ViewStep* step ) emit endInsertRows(); } - void ViewManager::onInstallationFailed( const QString& message, const QString& details ) { bool shouldOfferWebPaste = false; // TODO: config var - cError() << "Installation failed:"; - cDebug() << "- message:" << message; - cDebug() << "- details:" << details; + cError() << "Installation failed:" << message; + cDebug() << Logger::SubEntry << "- message:" << message; + cDebug() << Logger::SubEntry << "- details:" << Logger::NoQuote << details; QString heading = Calamares::Settings::instance()->isSetupMode() ? tr( "Setup Failed" ) : tr( "Installation Failed" ); @@ -152,7 +153,7 @@ ViewManager::onInstallationFailed( const QString& message, const QString& detail QString text = "

" + message + "

"; if ( !details.isEmpty() ) { - text += "

" + details + "

"; + text += "

" + CalamaresUtils::truncateMultiLine( details, CalamaresUtils::LinesStartEnd { 8, 0 } ) + "

"; } if ( shouldOfferWebPaste ) { @@ -365,8 +366,8 @@ ViewManager::next() { // Reached the end in a weird state (e.g. no finished step after an exec) executing = false; - UPDATE_BUTTON_PROPERTY( nextEnabled, false ) - UPDATE_BUTTON_PROPERTY( backEnabled, false ) + UPDATE_BUTTON_PROPERTY( nextEnabled, false ); + UPDATE_BUTTON_PROPERTY( backEnabled, false ); } updateCancelEnabled( !settings->disableCancel() && !( executing && settings->disableCancelDuringExec() ) ); updateBackAndNextVisibility( !( executing && settings->hideBackAndNextDuringExec() ) ); @@ -378,8 +379,8 @@ ViewManager::next() if ( m_currentStep < m_steps.count() ) { - UPDATE_BUTTON_PROPERTY( nextEnabled, !executing && m_steps.at( m_currentStep )->isNextEnabled() ) - UPDATE_BUTTON_PROPERTY( backEnabled, !executing && m_steps.at( m_currentStep )->isBackEnabled() ) + UPDATE_BUTTON_PROPERTY( nextEnabled, !executing && m_steps.at( m_currentStep )->isNextEnabled() ); + UPDATE_BUTTON_PROPERTY( backEnabled, !executing && m_steps.at( m_currentStep )->isBackEnabled() ); } updateButtonLabels(); @@ -401,26 +402,26 @@ ViewManager::updateButtonLabels() // If we're going into the execution step / install phase, other message if ( stepIsExecute( m_steps, m_currentStep + 1 ) ) { - UPDATE_BUTTON_PROPERTY( nextLabel, nextIsInstallationStep ) - UPDATE_BUTTON_PROPERTY( nextIcon, "run-install" ) + UPDATE_BUTTON_PROPERTY( nextLabel, nextIsInstallationStep ); + UPDATE_BUTTON_PROPERTY( nextIcon, "run-install" ); } else { - UPDATE_BUTTON_PROPERTY( nextLabel, tr( "&Next" ) ) - UPDATE_BUTTON_PROPERTY( nextIcon, "go-next" ) + UPDATE_BUTTON_PROPERTY( nextLabel, tr( "&Next" ) ); + UPDATE_BUTTON_PROPERTY( nextIcon, "go-next" ); } // Going back is always simple - UPDATE_BUTTON_PROPERTY( backLabel, tr( "&Back" ) ) - UPDATE_BUTTON_PROPERTY( backIcon, "go-previous" ) + UPDATE_BUTTON_PROPERTY( backLabel, tr( "&Back" ) ); + UPDATE_BUTTON_PROPERTY( backIcon, "go-previous" ); // Cancel button changes label at the end if ( isAtVeryEnd( m_steps, m_currentStep ) ) { - UPDATE_BUTTON_PROPERTY( quitLabel, tr( "&Done" ) ) - UPDATE_BUTTON_PROPERTY( quitTooltip, quitOnCompleteTooltip ) - UPDATE_BUTTON_PROPERTY( quitVisible, true ) - UPDATE_BUTTON_PROPERTY( quitIcon, "dialog-ok-apply" ) + UPDATE_BUTTON_PROPERTY( quitLabel, tr( "&Done" ) ); + UPDATE_BUTTON_PROPERTY( quitTooltip, quitOnCompleteTooltip ); + UPDATE_BUTTON_PROPERTY( quitVisible, true ); + UPDATE_BUTTON_PROPERTY( quitIcon, "dialog-ok-apply" ); updateCancelEnabled( true ); if ( settings->quitAtEnd() ) { @@ -431,14 +432,14 @@ ViewManager::updateButtonLabels() { if ( settings->disableCancel() ) { - UPDATE_BUTTON_PROPERTY( quitVisible, false ) + UPDATE_BUTTON_PROPERTY( quitVisible, false ); } updateCancelEnabled( !settings->disableCancel() && !( stepIsExecute( m_steps, m_currentStep ) && settings->disableCancelDuringExec() ) ); - UPDATE_BUTTON_PROPERTY( quitLabel, tr( "&Cancel" ) ) - UPDATE_BUTTON_PROPERTY( quitTooltip, cancelBeforeInstallationTooltip ) - UPDATE_BUTTON_PROPERTY( quitIcon, "dialog-cancel" ) + UPDATE_BUTTON_PROPERTY( quitLabel, tr( "&Cancel" ) ); + UPDATE_BUTTON_PROPERTY( quitTooltip, cancelBeforeInstallationTooltip ); + UPDATE_BUTTON_PROPERTY( quitIcon, "dialog-cancel" ); } } @@ -468,11 +469,11 @@ ViewManager::back() return; } - UPDATE_BUTTON_PROPERTY( nextEnabled, m_steps.at( m_currentStep )->isNextEnabled() ) + UPDATE_BUTTON_PROPERTY( nextEnabled, m_steps.at( m_currentStep )->isNextEnabled() ); UPDATE_BUTTON_PROPERTY( backEnabled, ( m_currentStep == 0 && m_steps.first()->isAtBeginning() ) ? false - : m_steps.at( m_currentStep )->isBackEnabled() ) + : m_steps.at( m_currentStep )->isBackEnabled() ); updateButtonLabels(); } @@ -525,14 +526,14 @@ ViewManager::confirmCancelInstallation() void ViewManager::updateCancelEnabled( bool enabled ) { - UPDATE_BUTTON_PROPERTY( quitEnabled, enabled ) + UPDATE_BUTTON_PROPERTY( quitEnabled, enabled ); emit cancelEnabled( enabled ); } void -ViewManager::updateBackAndNextVisibility( bool visible) +ViewManager::updateBackAndNextVisibility( bool visible ) { - UPDATE_BUTTON_PROPERTY( backAndNextVisible, visible ) + UPDATE_BUTTON_PROPERTY( backAndNextVisible, visible ); } QVariant diff --git a/src/modules/fstab/main.py b/src/modules/fstab/main.py index 6c2168a8e..6977ccad1 100644 --- a/src/modules/fstab/main.py +++ b/src/modules/fstab/main.py @@ -183,7 +183,7 @@ class FstabGenerator(object): print(FSTAB_HEADER, file=fstab_file) for partition in self.partitions: - # Special treatment for a btrfs root with @ and @home + # Special treatment for a btrfs root with @, @home and @swap # subvolumes if (partition["fs"] == "btrfs" and partition["mountPoint"] == "/"): @@ -206,6 +206,13 @@ class FstabGenerator(object): dct = self.generate_fstab_line_info(home_entry) if dct: self.print_fstab_line(dct, file=fstab_file) + elif line.endswith(b'path @swap'): + swap_part_entry = partition + swap_part_entry["mountPoint"] = "/swap" + swap_part_entry["subvol"] = "@swap" + dct = self.generate_fstab_line_info(swap_part_entry) + if dct: + self.print_fstab_line(dct, file=fstab_file) else: dct = self.generate_fstab_line_info(partition) @@ -319,14 +326,19 @@ def create_swapfile(root_mount_point, root_btrfs): The swapfile-creation covers progress from 0.2 to 0.5 """ libcalamares.job.setprogress(0.2) - swapfile_path = os.path.join(root_mount_point, "swapfile") - with open(swapfile_path, "wb") as f: - pass if root_btrfs: + # btrfs swapfiles must reside on a subvolume that is not snapshotted to prevent file system corruption + swapfile_path = os.path.join(root_mount_point, "swap/swapfile") + with open(swapfile_path, "wb") as f: + pass o = subprocess.check_output(["chattr", "+C", swapfile_path]) libcalamares.utils.debug("swapfile attributes: {!s}".format(o)) o = subprocess.check_output(["btrfs", "property", "set", swapfile_path, "compression", "none"]) libcalamares.utils.debug("swapfile compression: {!s}".format(o)) + else: + swapfile_path = os.path.join(root_mount_point, "swapfile") + with open(swapfile_path, "wb") as f: + pass # Create the swapfile; swapfiles are small-ish zeroes = bytes(16384) with open(swapfile_path, "wb") as f: @@ -374,7 +386,12 @@ def run(): swap_choice = swap_choice.get( "swap", None ) if swap_choice and swap_choice == "file": # There's no formatted partition for it, so we'll sneak in an entry - partitions.append( dict(fs="swap", mountPoint=None, claimed=True, device="/swapfile", uuid=None) ) + root_partitions = [ p["fs"].lower() for p in partitions if p["mountPoint"] == "/" ] + root_btrfs = (root_partitions[0] == "btrfs") if root_partitions else False + if root_btrfs: + partitions.append( dict(fs="swap", mountPoint=None, claimed=True, device="/swap/swapfile", uuid=None) ) + else: + partitions.append( dict(fs="swap", mountPoint=None, claimed=True, device="/swapfile", uuid=None) ) else: swap_choice = None diff --git a/src/modules/mount/main.py b/src/modules/mount/main.py index 1313fca49..3982176df 100644 --- a/src/modules/mount/main.py +++ b/src/modules/mount/main.py @@ -38,6 +38,7 @@ def mount_partition(root_mount_point, partition, partitions): """ # Create mount point with `+` rather than `os.path.join()` because # `partition["mountPoint"]` starts with a '/'. + global_storage = libcalamares.globalstorage raw_mount_point = partition["mountPoint"] if not raw_mount_point: return @@ -77,6 +78,7 @@ def mount_partition(root_mount_point, partition, partitions): # for the root mount point. # If a separate /home partition isn't defined, we also create # a subvolume "@home". + # If a swapfile is used, we also create a subvolume "@swap". # Finally we remount all of the above on the correct paths. if fstype == "btrfs" and partition["mountPoint"] == '/': has_home_mount_point = False @@ -86,6 +88,12 @@ def mount_partition(root_mount_point, partition, partitions): if p["mountPoint"] == "/home": has_home_mount_point = True break + needs_swap_subvolume = False + swap_choice = global_storage.value( "partitionChoices" ) + if swap_choice: + swap_choice = swap_choice.get( "swap", None ) + if swap_choice and swap_choice == "file": + needs_swap_subvolume = True subprocess.check_call(['btrfs', 'subvolume', 'create', root_mount_point + '/@']) @@ -93,6 +101,9 @@ def mount_partition(root_mount_point, partition, partitions): if not has_home_mount_point: subprocess.check_call(['btrfs', 'subvolume', 'create', root_mount_point + '/@home']) + if needs_swap_subvolume: + subprocess.check_call(['btrfs', 'subvolume', 'create', + root_mount_point + '/@swap']) subprocess.check_call(["umount", "-v", root_mount_point]) @@ -113,6 +124,13 @@ def mount_partition(root_mount_point, partition, partitions): fstype, ",".join(["subvol=@home", partition.get("options", "")])) != 0: libcalamares.utils.warning("Cannot mount {}".format(device)) + + if needs_swap_subvolume: + if libcalamares.utils.mount(device, + root_mount_point + "/swap", + fstype, + ",".join(["subvol=@swap", partition.get("options", "")])) != 0: + libcalamares.utils.warning("Cannot mount {}".format(device)) def run(): diff --git a/src/modules/shellprocess/shellprocess.schema.yaml b/src/modules/shellprocess/shellprocess.schema.yaml new file mode 100644 index 000000000..d92ebfc83 --- /dev/null +++ b/src/modules/shellprocess/shellprocess.schema.yaml @@ -0,0 +1,54 @@ +$schema: http://json-schema.org/draft-07/schema# +$id: https://calamares.io/schemas/shellprocess +definitions: + command: + $id: '#definitions/command' + type: string + description: This is one command that is executed. If a command starts with '-' + (a single minus sign), then the return value of the command following the - + is ignored; otherwise, a failing command will abort the installation. + commandObj: + $id: '#definitions/commandObj' + type: object + properties: + command: + $ref: '#definitions/command' + timeout: + type: number + description: the (optional) timeout for this specific command (differently + from the global setting) + required: + - command +type: object +description: Configuration for the shell process job. +properties: + dontChroot: + type: boolean + description: If the top-level key *dontChroot* is true, then the commands are + executed in the context of the live system, otherwise in the context of the + target system. + timeout: + type: number + description: The (global) timeout for the command list in seconds. If unset, defaults + to 30 seconds. + script: + anyOf: + - $ref: '#definitions/command' + - $ref: '#definitions/commandObj' + - type: array + description: these commands are executed one at a time, by separate shells (/bin/sh + -c is invoked for each command). + items: + anyOf: + - $ref: '#definitions/command' + - $ref: '#definitions/commandObj' + i18n: + type: object + description: To change description of the job (as it is displayed in the progress + bar during installation) use *name* field and optionally, translations as *name[lang]*. + Without a translation, the default name from the source code is used, "Shell Processes Job". + properties: + name: + type: string + required: + - name