diff --git a/src/modules/license/CMakeLists.txt b/src/modules/license/CMakeLists.txt index 54774dede..164233b3b 100644 --- a/src/modules/license/CMakeLists.txt +++ b/src/modules/license/CMakeLists.txt @@ -1,18 +1,12 @@ include_directories( ${PROJECT_BINARY_DIR}/src/libcalamaresui ) + calamares_add_plugin( license - -set_source_files_properties( PROPERTIES LANGUAGE CXX ) - -find_package( Qt5 ${QT_VERSION} CONFIG REQUIRED DBus ) - -set( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} - ${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules ) - TYPE viewmodule EXPORT_MACRO PLUGINDLLEXPORT_PRO SOURCES - LicenseViewStep.cpp LicensePage.cpp + LicenseViewStep.cpp + LicenseWidget.cpp UI LicensePage.ui LINK_PRIVATE_LIBRARIES diff --git a/src/modules/license/LicensePage.cpp b/src/modules/license/LicensePage.cpp index 1fb3de7a8..0ed5a6a17 100644 --- a/src/modules/license/LicensePage.cpp +++ b/src/modules/license/LicensePage.cpp @@ -22,13 +22,18 @@ #include "LicensePage.h" #include "ui_LicensePage.h" +#include "LicenseWidget.h" + #include "JobQueue.h" #include "GlobalStorage.h" -#include "utils/Logger.h" -#include "utils/CalamaresUtilsGui.h" -#include "utils/Retranslator.h" #include "ViewManager.h" +#include "utils/CalamaresUtils.h" +#include "utils/CalamaresUtilsGui.h" +#include "utils/Logger.h" +#include "utils/NamedEnum.h" +#include "utils/Retranslator.h" + #include #include #include @@ -37,6 +42,49 @@ #include #include +#include + +const NamedEnumTable< LicenseEntry::Type >& +LicenseEntry::typeNames() +{ + static const NamedEnumTable< LicenseEntry::Type > names{ + { QStringLiteral( "software" ), LicenseEntry::Type::Software}, + { QStringLiteral( "driver" ), LicenseEntry::Type::Driver }, + { QStringLiteral( "gpudriver" ), LicenseEntry::Type::GpuDriver }, + { QStringLiteral( "browserplugin" ), LicenseEntry::Type::BrowserPlugin}, + { QStringLiteral( "codec" ), LicenseEntry::Type::Codec }, + { QStringLiteral( "package" ), LicenseEntry::Type::Package } + }; + + return names; +} + +LicenseEntry::LicenseEntry(const QVariantMap& conf) +{ + if ( !conf.contains( "id" ) || !conf.contains( "name" ) || !conf.contains( "url" ) ) + return; + + m_id = conf[ "id" ].toString(); + m_prettyName = conf[ "name" ].toString(); + m_prettyVendor = conf.value( "vendor" ).toString(); + m_url = QUrl( conf[ "url" ].toString() ); + + m_required = CalamaresUtils::getBool( conf, "required", false ); + + bool ok = false; + QString typeString = conf.value( "type", "software" ).toString(); + m_type = typeNames().find( typeString, ok ); + if ( !ok ) + cWarning() << "License entry" << m_id << "has unknown type" << typeString << "(using 'software')"; +} + +bool +LicenseEntry::isLocal() const +{ + return m_url.isLocalFile(); +} + + LicensePage::LicensePage(QWidget *parent) : QWidget( parent ) , ui( new Ui::LicensePage ) @@ -66,24 +114,9 @@ LicensePage::LicensePage(QWidget *parent) "padding: 2px; }" ); ui->acceptFrame->layout()->setMargin( CalamaresUtils::defaultFontHeight() / 2 ); - connect( ui->acceptCheckBox, &QCheckBox::toggled, - this, [ this ]( bool checked ) - { - Calamares::JobQueue::instance()->globalStorage()->insert( "licenseAgree", checked ); - m_isNextEnabled = checked; - if ( !checked ) - { - ui->acceptFrame->setStyleSheet( "#acceptFrame { border: 1px solid red;" - "background-color: #fff8f8;" - "border-radius: 4px;" - "padding: 2px; }" ); - } - else - { - ui->acceptFrame->setStyleSheet( "#acceptFrame { padding: 3px }" ); - } - emit nextStatusChanged( checked ); - } ); + updateGlobalStorage( false ); // Have not agreed yet + + connect( ui->acceptCheckBox, &QCheckBox::toggled, this, &LicensePage::checkAcceptance ); CALAMARES_RETRANSLATE( ui->acceptCheckBox->setText( tr( "I accept the terms and conditions above." ) ); @@ -95,19 +128,16 @@ void LicensePage::setEntries( const QList< LicenseEntry >& entriesList ) { CalamaresUtils::clearLayout( ui->licenseEntriesLayout ); + m_entries.clear(); + m_entries.reserve( entriesList.count() ); - bool required = false; - for ( const LicenseEntry& entry : entriesList ) - { - if ( entry.required ) - { - required = true; - break; - } - } + const bool required = std::any_of( entriesList.cbegin(), entriesList.cend(), []( const LicenseEntry& e ){ return e.m_required; }); + if ( entriesList.isEmpty() ) + m_allLicensesOptional = true; + else + m_allLicensesOptional = !required; - m_isNextEnabled = !required; - nextStatusChanged( m_isNextEnabled ); + checkAcceptance( false ); CALAMARES_RETRANSLATE( if ( required ) @@ -132,77 +162,16 @@ LicensePage::setEntries( const QList< LicenseEntry >& entriesList ) "be installed, and open source alternatives will be used instead." ) ); } ui->retranslateUi( this ); + + for ( const auto& w : m_entries ) + w->retranslateUi(); ) for ( const LicenseEntry& entry : entriesList ) { - QWidget* widget = new QWidget( this ); - QPalette pal( palette() ); - pal.setColor( QPalette::Background, palette().background().color().lighter( 108 ) ); - widget->setAutoFillBackground( true ); - widget->setPalette( pal ); - widget->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum ); - widget->setContentsMargins( 4, 4, 4, 4 ); - - QHBoxLayout* wiLayout = new QHBoxLayout; - widget->setLayout( wiLayout ); - QLabel* label = new QLabel( widget ); - label->setWordWrap( true ); - wiLayout->addWidget( label ); - label->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Minimum ); - - QString productDescription; - switch ( entry.type ) - { - case LicenseEntry::Driver: - //: %1 is an untranslatable product name, example: Creative Audigy driver - productDescription = tr( "%1 driver
" - "by %2" ) - .arg( entry.prettyName ) - .arg( entry.prettyVendor ); - break; - case LicenseEntry::GpuDriver: - //: %1 is usually a vendor name, example: Nvidia graphics driver - productDescription = tr( "%1 graphics driver
" - "by %2" ) - .arg( entry.prettyName ) - .arg( entry.prettyVendor ); - break; - case LicenseEntry::BrowserPlugin: - productDescription = tr( "%1 browser plugin
" - "by %2" ) - .arg( entry.prettyName ) - .arg( entry.prettyVendor ); - break; - case LicenseEntry::Codec: - productDescription = tr( "%1 codec
" - "by %2" ) - .arg( entry.prettyName ) - .arg( entry.prettyVendor ); - break; - case LicenseEntry::Package: - productDescription = tr( "%1 package
" - "by %2" ) - .arg( entry.prettyName ) - .arg( entry.prettyVendor ); - break; - case LicenseEntry::Software: - productDescription = tr( "%1
" - "by %2" ) - .arg( entry.prettyName ) - .arg( entry.prettyVendor ); - } - label->setText( productDescription ); - - QLabel* viewLicenseLabel = new QLabel( widget ); - wiLayout->addWidget( viewLicenseLabel ); - viewLicenseLabel->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Preferred ); - viewLicenseLabel->setOpenExternalLinks( true ); - viewLicenseLabel->setAlignment( Qt::AlignVCenter | Qt::AlignRight ); - viewLicenseLabel->setText( tr( "view license agreement" ) - .arg( entry.url.toString() ) ); - - ui->licenseEntriesLayout->addWidget( widget ); + LicenseWidget* w = new LicenseWidget( entry ); + ui->licenseEntriesLayout->addWidget( w ); + m_entries.append( w ); } ui->licenseEntriesLayout->addStretch(); } @@ -213,3 +182,28 @@ LicensePage::isNextEnabled() const { return m_isNextEnabled; } + +void +LicensePage::updateGlobalStorage( bool v ) +{ + Calamares::JobQueue::instance()->globalStorage()->insert( "licenseAgree", v ); +} + +void LicensePage::checkAcceptance( bool checked ) +{ + updateGlobalStorage( checked ); + + m_isNextEnabled = checked || m_allLicensesOptional; + if ( !m_isNextEnabled ) + { + ui->acceptFrame->setStyleSheet( "#acceptFrame { border: 1px solid red;" + "background-color: #fff8f8;" + "border-radius: 4px;" + "padding: 2px; }" ); + } + else + { + ui->acceptFrame->setStyleSheet( "#acceptFrame { padding: 3px }" ); + } + emit nextStatusChanged( checked ); +} diff --git a/src/modules/license/LicensePage.h b/src/modules/license/LicensePage.h index 300e9e309..c08676f92 100644 --- a/src/modules/license/LicensePage.h +++ b/src/modules/license/LicensePage.h @@ -22,6 +22,8 @@ #ifndef LICENSEPAGE_H #define LICENSEPAGE_H +#include "utils/NamedEnum.h" + #include #include @@ -30,9 +32,11 @@ namespace Ui class LicensePage; } +class LicenseWidget; + struct LicenseEntry { - enum Type + enum class Type { Software = 0, Driver, @@ -42,12 +46,22 @@ struct LicenseEntry Package }; - QString id; - QString prettyName; - QString prettyVendor; - Type type; - QUrl url; - bool required; + /// @brief Lookup table for the enums + const NamedEnumTable< Type >& typeNames(); + + LicenseEntry( const QVariantMap& conf ); + LicenseEntry( const LicenseEntry& ) = default; + + bool isValid() const { return !m_id.isEmpty(); } + bool isRequired() const { return m_required; } + bool isLocal() const; + + QString m_id; + QString m_prettyName; + QString m_prettyVendor; + Type m_type; + QUrl m_url; + bool m_required; }; class LicensePage : public QWidget @@ -59,13 +73,31 @@ public: void setEntries( const QList< LicenseEntry >& entriesList ); bool isNextEnabled() const; + +public slots: + /** @brief Check if the user can continue + * + * The user can continue if + * - none of the licenses are required, or + * - the user has ticked the "OK" box. + * This function calls updateGlobalStorage() as needed, and updates + * the appearance of the page as needed. @p checked indicates whether + * the checkbox has been ticked or not. + */ + void checkAcceptance( bool checked ); + signals: void nextStatusChanged( bool status ); private: - Ui::LicensePage* ui; + /** @brief Update the global storage "licenseAgree" key. */ + void updateGlobalStorage( bool v ); bool m_isNextEnabled; + bool m_allLicensesOptional; //< all the licenses passed to setEntries are not-required + + Ui::LicensePage* ui; + QList< LicenseWidget* > m_entries; }; #endif //LICENSEPAGE_H diff --git a/src/modules/license/LicensePage.ui b/src/modules/license/LicensePage.ui index 9de62511e..767b392a0 100644 --- a/src/modules/license/LicensePage.ui +++ b/src/modules/license/LicensePage.ui @@ -46,7 +46,31 @@ - + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 765 + 94 + + + + + diff --git a/src/modules/license/LicenseViewStep.cpp b/src/modules/license/LicenseViewStep.cpp index 96a7b8660..f5f4b6e2b 100644 --- a/src/modules/license/LicenseViewStep.cpp +++ b/src/modules/license/LicenseViewStep.cpp @@ -106,34 +106,9 @@ LicenseViewStep::setConfigurationMap( const QVariantMap& configurationMap ) if ( entryV.type() != QVariant::Map ) continue; - QVariantMap entryMap = entryV.toMap(); - if ( !entryMap.contains( "id" ) || - !entryMap.contains( "name" ) || - !entryMap.contains( "url" ) ) - continue; - - LicenseEntry entry; - entry.id = entryMap[ "id" ].toString(); - entry.prettyName = entryMap[ "name" ].toString(); - entry.prettyVendor =entryMap.value( "vendor" ).toString(); - entry.url = QUrl( entryMap[ "url" ].toString() ); - entry.required = entryMap.value( "required", QVariant( false ) ).toBool(); - - QString entryType = entryMap.value( "type", "software" ).toString(); - if ( entryType == "driver" ) - entry.type = LicenseEntry::Driver; - else if ( entryType == "gpudriver" ) - entry.type = LicenseEntry::GpuDriver; - else if ( entryType == "browserplugin" ) - entry.type = LicenseEntry::BrowserPlugin; - else if ( entryType == "codec" ) - entry.type = LicenseEntry::Codec; - else if ( entryType == "package" ) - entry.type = LicenseEntry::Package; - else - entry.type = LicenseEntry::Software; - - entriesList.append( entry ); + LicenseEntry entry( entryV.toMap() ); + if ( entry.isValid() ) + entriesList.append( entry ); } } diff --git a/src/modules/license/LicenseWidget.cpp b/src/modules/license/LicenseWidget.cpp new file mode 100644 index 000000000..238d57b07 --- /dev/null +++ b/src/modules/license/LicenseWidget.cpp @@ -0,0 +1,211 @@ +/* === This file is part of Calamares - === + * + * Copyright 2015, Anke Boersma + * Copyright 2015, Alexandre Arnt + * Copyright 2015, Teo Mrnjavac + * Copyright 2018, Adriaan de Groot + * + * 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 . + */ + +#include "LicenseWidget.h" + +#include "utils/Logger.h" + +#include +#include +#include +#include +#include +#include + +static QString +loadLocalFile( const QUrl& u ) +{ + if ( !u.isLocalFile() ) + return QString(); + + QFile file( u.path() ); + if ( !file.open(QIODevice::ReadOnly | QIODevice::Text) ) + { + cWarning() << "Could not load license file" << u.path(); + return QString(); + } + + return QString( "\n" ) + file.readAll(); +} + +LicenseWidget::LicenseWidget( LicenseEntry entry, QWidget* parent ) + : QWidget( parent ) + , m_entry( std::move( entry ) ) + , m_label( new QLabel( this ) ) + , m_viewLicenseLabel( new QLabel( this ) ) + , m_expandLicenseButton( nullptr ) + , m_fullText( nullptr ) +{ + QPalette pal( palette() ); + pal.setColor( QPalette::Background, palette().background().color().lighter( 108 ) ); + + setObjectName( "licenseItem" ); + + setAutoFillBackground( true ); + setPalette( pal ); + setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum ); + setContentsMargins( 4, 4, 4, 4 ); + + QHBoxLayout* wiLayout = new QHBoxLayout; + + m_label->setWordWrap( true ); + m_label->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Minimum ); + wiLayout->addWidget( m_label ); + + m_viewLicenseLabel->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Preferred ); + m_viewLicenseLabel->setAlignment( Qt::AlignVCenter | Qt::AlignRight ); + wiLayout->addWidget( m_viewLicenseLabel ); + + m_expandLicenseButton = new QToolButton( this ); + wiLayout->addWidget( m_expandLicenseButton ); + if ( m_entry.isLocal() ) + { + QVBoxLayout* vLayout = new QVBoxLayout; + + m_expandLicenseButton->setArrowType( Qt::UpArrow ); + connect( m_expandLicenseButton, &QAbstractButton::clicked, this, &LicenseWidget::expandClicked ); + + vLayout->addLayout( wiLayout ); + m_fullText = new QLabel( this ); + m_fullText->setText( loadLocalFile( m_entry.m_url ) ); + m_fullText->hide(); + m_fullText->setStyleSheet( "border-top: 1px solid black; margin-top: 1em; padding-top: 1em;" ); + m_fullText->setObjectName( "licenseItemFullText" ); + + vLayout->addWidget( m_fullText ); + setLayout( vLayout ); + } + else + { + m_expandLicenseButton->setArrowType( Qt::RightArrow ); + connect( m_expandLicenseButton, &QAbstractButton::clicked, this, &LicenseWidget::viewClicked ); + + // Normally setOpenExternalLinks( true ) would do, but we need the + // open code anyway for the toolbutton, let's share it. + connect( m_viewLicenseLabel, &QLabel::linkActivated, this, &LicenseWidget::viewClicked ); + + setLayout( wiLayout ); // Only the horizontal layout needed + } + + retranslateUi(); +} + +LicenseWidget::~LicenseWidget() +{ +} + +void LicenseWidget::retranslateUi() +{ + QString productDescription; + switch ( m_entry.m_type ) + { + case LicenseEntry::Type::Driver: + //: %1 is an untranslatable product name, example: Creative Audigy driver + productDescription = tr( "%1 driver
" + "by %2" ) + .arg( m_entry.m_prettyName ) + .arg( m_entry.m_prettyVendor ); + break; + case LicenseEntry::Type::GpuDriver: + //: %1 is usually a vendor name, example: Nvidia graphics driver + productDescription = tr( "%1 graphics driver
" + "by %2" ) + .arg( m_entry.m_prettyName ) + .arg( m_entry.m_prettyVendor ); + break; + case LicenseEntry::Type::BrowserPlugin: + productDescription = tr( "%1 browser plugin
" + "by %2" ) + .arg( m_entry.m_prettyName ) + .arg( m_entry.m_prettyVendor ); + break; + case LicenseEntry::Type::Codec: + productDescription = tr( "%1 codec
" + "by %2" ) + .arg( m_entry.m_prettyName ) + .arg( m_entry.m_prettyVendor ); + break; + case LicenseEntry::Type::Package: + productDescription = tr( "%1 package
" + "by %2" ) + .arg( m_entry.m_prettyName ) + .arg( m_entry.m_prettyVendor ); + break; + case LicenseEntry::Type::Software: + productDescription = tr( "%1
" + "by %2" ) + .arg( m_entry.m_prettyName ) + .arg( m_entry.m_prettyVendor ); + } + m_label->setText( productDescription ); + updateExpandToolTip(); +} + +void +LicenseWidget::expandClicked() +{ + if ( m_expandLicenseButton->arrowType() == Qt::DownArrow ) + { + m_expandLicenseButton->setArrowType( Qt::UpArrow ); + } + else + { + m_expandLicenseButton->setArrowType( Qt::DownArrow ); + } + + // Show/hide based on the new arrow direction. + if ( m_fullText ) + m_fullText->setHidden( m_expandLicenseButton->arrowType() == Qt::UpArrow ); + + updateExpandToolTip(); +} + +/** @brief Called on retranslate and when button state changes. */ +void +LicenseWidget::updateExpandToolTip() +{ + if ( m_entry.isLocal() ) + { + const bool isNowCollapsed = m_expandLicenseButton->arrowType() == Qt::UpArrow; + + m_expandLicenseButton->setToolTip( + isNowCollapsed + ? tr( "Shows the complete license text" ) + : tr( "Hide license text" ) + ) ; + m_viewLicenseLabel->setText( + isNowCollapsed + ? tr( "Show license agreement" ) + : tr( "Hide license agreement" ) ); + } + else + { + m_expandLicenseButton->setToolTip( tr( "Opens the license agreement in a browser window." ) ); + m_viewLicenseLabel->setText( tr( "View license agreement" ) + .arg( m_entry.m_url.toString() ) ); + } +} + +void +LicenseWidget::viewClicked() +{ + QDesktopServices::openUrl( m_entry.m_url ); +} diff --git a/src/modules/license/LicenseWidget.h b/src/modules/license/LicenseWidget.h new file mode 100644 index 000000000..c43233da4 --- /dev/null +++ b/src/modules/license/LicenseWidget.h @@ -0,0 +1,51 @@ +/* === This file is part of Calamares - === + * + * Copyright 2015, Anke Boersma + * Copyright 2015, Alexandre Arnt + * Copyright 2015, Teo Mrnjavac + * Copyright 2018, Adriaan de Groot + * + * 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 . + */ + +#ifndef LICENSE_LICENSEWIDGET_H +#define LICENSE_LICENSEWIDGET_H + +#include "LicensePage.h" + +#include +#include + +class QToolButton; + +class LicenseWidget : public QWidget +{ +public: + LicenseWidget( LicenseEntry e, QWidget* parent = nullptr ); + virtual ~LicenseWidget() override; + + void retranslateUi(); + +private: + void expandClicked(); // "slot" to toggle show/hide of local license text + void viewClicked(); // "slot" to open link + void updateExpandToolTip(); + + LicenseEntry m_entry; + QLabel* m_label; + QLabel* m_viewLicenseLabel; + QToolButton* m_expandLicenseButton; + QLabel* m_fullText; +} ; +#endif diff --git a/src/modules/license/license.conf b/src/modules/license/license.conf index c99b1529d..9057f8a51 100644 --- a/src/modules/license/license.conf +++ b/src/modules/license/license.conf @@ -1,14 +1,26 @@ # Configuration file for License viewmodule, Calamares # Syntax is YAML 1.2 --- -# YAML: list of maps. +# Define a list of licenses which may / must be accepted before continuing. +# +# Each entry in this list has the following keys: +# - id Entry identifier, must be unique. Not user visible. YAML: string. +# - name Pretty name for the software product, user visible and untranslatable. YAML: string. +# - vendor Pretty name for the software vendor, user visible and untranslatable. YAML: string, optional, default is empty. +# - type Package type identifier for presentation, not user visible but affects user visible strings. YAML: string. +# values: driver, gpudriver, browserplugin, codec, package, software; optional, default is software. +# - required If set to true, the user cannot proceed without accepting this license. YAML: boolean, optional, default is false. +# - url A URL for the license; a remote URL is not shown in Calamares, but a link +# to the URL is provided, which opens in the default web browser. A local +# URL (i.e. file:///) assumes that the contents are HTML or plain text, and +# displays the license in-line. YAML: string, mandatory. entries: -- id: nvidia # Entry identifier, must be unique. Not user visible. YAML: string. - name: Nvidia # Pretty name for the software product, user visible and untranslatable. YAML: string. - vendor: Nvidia Corporation # Pretty name for the software vendor, user visible and untranslatable. YAML: string, optional, default is empty. - type: driver # Package type for presentation, not user visible but affects user visible strings. YAML: string, allowed values: driver, gpudriver, browserplugin, codec, package, software; optional, default is software. - url: http://developer.download.nvidia.com/cg/Cg_3.0/license.pdf # Url of license text to display in a web view. YAML: string. - required: false # If set to true, the user cannot proceed without accepting this license. YAML: boolean, optional, default is false. +- id: nvidia + name: Nvidia + vendor: Nvidia Corporation + type: driver + url: http://developer.download.nvidia.com/cg/Cg_3.0/license.pdf + required: false - id: amd name: Catalyst vendor: "Advanced Micro Devices, Inc." @@ -21,3 +33,13 @@ entries: type: browserplugin url: http://www.adobe.com/products/eulas/pdfs/PlatformClients_PC_WWEULA_Combined_20100108_1657.pdf required: true +# This example uses a file: link. This example uses a relative link, which +# is relative to where you run Calamares. Assuming you run it from build/ +# as part of your testing, you'll get the LICENSE text for Calamares +# (which is the text of the GPLv3, not proprietary at all). +- id: mine_mine + name: Calamares Proprietary License + vendor: Calamares, Inc. + type: software + required: true + url: file:../LICENSE