From 5a0dcda171772e75eeb22f719cec36ef8efe355d Mon Sep 17 00:00:00 2001 From: Ulf Hermann Date: Wed, 30 Nov 2022 14:45:48 +0100 Subject: [PATCH] CMake: Allow contents of resources to be replaced by empty files This makes it possible to process QML files using qmlcachegen, and retain the file nodes in the resource file system, but remove their actual content from the binary. To do so, you need to mark your files with the QT_DISCARD_FILE_CONTENTS source file property. Fixes: QTBUG-87676 Fixes: QTBUG-103481 Fixes: QTBUG-102024 Fixes: QTBUG-102785 Change-Id: I93d5a2bfca1739ff1e0f74c8082eb8aa451b9815 Reviewed-by: Alexandru Croitor Reviewed-by: hjk --- src/corelib/Qt6CoreMacros.cmake | 9 +++- .../doc/snippets/code/doc_src_resources.qdoc | 4 ++ .../doc/src/cmake/cmake-properties.qdoc | 27 +++++++++++ src/corelib/doc/src/resource-system.qdoc | 19 ++++++++ src/tools/rcc/rcc.cpp | 45 ++++++++++++++----- src/tools/rcc/rcc.h | 1 + .../corelib/io/qresourceengine/CMakeLists.txt | 11 +++++ .../qresourceengine/tst_qresourceengine.cpp | 10 +++++ .../auto/corelib/io/qresourceengine/world.txt | 1 + 9 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 tests/auto/corelib/io/qresourceengine/world.txt diff --git a/src/corelib/Qt6CoreMacros.cmake b/src/corelib/Qt6CoreMacros.cmake index a3d53e8fc0..fdea5d6324 100644 --- a/src/corelib/Qt6CoreMacros.cmake +++ b/src/corelib/Qt6CoreMacros.cmake @@ -1884,10 +1884,15 @@ function(_qt_internal_process_resource target resourceName) set(file "${CMAKE_CURRENT_SOURCE_DIR}/${file}") endif() + get_property(is_empty SOURCE ${file} PROPERTY QT_DISCARD_FILE_CONTENTS) + ### FIXME: escape file paths to be XML conform # ... - string(APPEND qrcContents " ") - string(APPEND qrcContents "${file}\n") + string(APPEND qrcContents " ${file}\n") list(APPEND files "${file}") set(scope_args) diff --git a/src/corelib/doc/snippets/code/doc_src_resources.qdoc b/src/corelib/doc/snippets/code/doc_src_resources.qdoc index a2e515ce03..f5a12ee86a 100644 --- a/src/corelib/doc/snippets/code/doc_src_resources.qdoc +++ b/src/corelib/doc/snippets/code/doc_src_resources.qdoc @@ -26,3 +26,7 @@ //! [3] rcc -binary myresource.qrc -o myresource.rcc //! [3] + +//! [4] +Button.qml +//! [4] diff --git a/src/corelib/doc/src/cmake/cmake-properties.qdoc b/src/corelib/doc/src/cmake/cmake-properties.qdoc index 89e7045bc7..42dfeeabe2 100644 --- a/src/corelib/doc/src/cmake/cmake-properties.qdoc +++ b/src/corelib/doc/src/cmake/cmake-properties.qdoc @@ -421,6 +421,33 @@ the property value overrides the runtime path where the resource file is found. \sa{The Qt Resource System} */ +/*! +\page cmake-source-file-property-qt-discard-file-contents.html +\ingroup cmake-source-file-properties-qtcore + +\title QT_DISCARD_FILE_CONTENTS +\target cmake-source-file-property-QT_DISCARD_FILE_CONTENTS + +\summary {Specifies that the given files should be empty in the resource file system} + +\cmakepropertysince 6.5 +\preliminarycmakeproperty + +When using the target-based variant of \l{qt6_add_resources}{qt_add_resources} +or \l{qt_add_qml_module}, setting this property to \c TRUE causes the file +contents to be omitted when creating the resource file system. The file name is +retained. + +This is useful if you want to strip QML source code from the binary. + +\note If you omit the QML source code from the binary, the QML engine has to +rely on the the compilation units created by \l{qmlcachegen} or \l{qmlsc}. +Those are tied to the specific version of Qt they were built with. If you change +the version of Qt your application uses, they can't be loaded anymore. + +\sa{The Qt Resource System} +*/ + /*! \page cmake-target-property-qt-wasm-pthread-pool-size.html \ingroup cmake-properties-qtcore diff --git a/src/corelib/doc/src/resource-system.qdoc b/src/corelib/doc/src/resource-system.qdoc index 8cee601c67..66d0c214b0 100644 --- a/src/corelib/doc/src/resource-system.qdoc +++ b/src/corelib/doc/src/resource-system.qdoc @@ -166,6 +166,25 @@ The file is from the application then only accessible as \c :/cut-img.png or \c{qrc:/cut-img.png}. + \section2 Discarding the file contents + + Sometimes you want to add a file node to the resource file system but + don't actually want to add the file contents. \c .qrc files allow this + by setting the \c empty attribute to \c{true}. + + \snippet code/doc_src_resources.qdoc 4 + + The resulting file is then still accessible from the application, but + its contents are empty. + + This is useful to strip QML source code from an application binary. + + \note If you omit the QML source code from the binary, the QML engine has to + rely on the the compilation units created by \l{qmlcachegen} or \l{qmlsc}. + Those are tied to the specific version of Qt they were built with. If you + change the version of Qt your application uses, they can't be loaded + anymore. + \section2 Language Selectors Some resources need to change based on the user's locale, such as diff --git a/src/tools/rcc/rcc.cpp b/src/tools/rcc/rcc.cpp index a87f15de33..3b5166a70e 100644 --- a/src/tools/rcc/rcc.cpp +++ b/src/tools/rcc/rcc.cpp @@ -82,7 +82,8 @@ public: RCCResourceLibrary::CompressionAlgorithm compressAlgo = RCCResourceLibrary::CompressionAlgorithm::Best, int compressLevel = CONSTANT_COMPRESSLEVEL_DEFAULT, int compressThreshold = CONSTANT_COMPRESSTHRESHOLD_DEFAULT, - bool noZstd = false); + bool noZstd = false, + bool isEmpty = false); ~RCCFileInfo(); QString resourceName() const; @@ -107,12 +108,13 @@ public: qint64 m_dataOffset; qint64 m_childOffset; bool m_noZstd; + bool m_isEmpty; }; RCCFileInfo::RCCFileInfo(const QString &name, const QFileInfo &fileInfo, QLocale::Language language, QLocale::Territory territory, uint flags, RCCResourceLibrary::CompressionAlgorithm compressAlgo, int compressLevel, int compressThreshold, - bool noZstd) + bool noZstd, bool isEmpty) { m_name = name; m_fileInfo = fileInfo; @@ -127,6 +129,7 @@ RCCFileInfo::RCCFileInfo(const QString &name, const QFileInfo &fileInfo, m_compressLevel = compressLevel; m_compressThreshold = compressThreshold; m_noZstd = noZstd; + m_isEmpty = isEmpty; } RCCFileInfo::~RCCFileInfo() @@ -225,14 +228,18 @@ qint64 RCCFileInfo::writeDataBlob(RCCResourceLibrary &lib, qint64 offset, //capture the offset m_dataOffset = offset; + QByteArray data; - //find the data to be written - QFile file(m_fileInfo.absoluteFilePath()); - if (!file.open(QFile::ReadOnly)) { - *errorMessage = msgOpenReadFailed(m_fileInfo.absoluteFilePath(), file.errorString()); - return 0; + if (!m_isEmpty) { + //find the data to be written + QFile file(m_fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) { + *errorMessage = msgOpenReadFailed(m_fileInfo.absoluteFilePath(), file.errorString()); + return 0; + } + + data = file.readAll(); } - QByteArray data = file.readAll(); // Check if compression is useful for this file if (data.size() != 0) { @@ -420,6 +427,7 @@ RCCResourceLibrary::Strings::Strings() : ATTRIBUTE_LANG("lang"_L1), ATTRIBUTE_PREFIX("prefix"_L1), ATTRIBUTE_ALIAS("alias"_L1), + ATTRIBUTE_EMPTY("empty"_L1), ATTRIBUTE_THRESHOLD("threshold"_L1), ATTRIBUTE_COMPRESS("compress"_L1), ATTRIBUTE_COMPRESSALGO(QStringLiteral("compression-algorithm")) @@ -464,6 +472,17 @@ enum RCCXmlTag { }; Q_DECLARE_TYPEINFO(RCCXmlTag, Q_PRIMITIVE_TYPE); +static bool parseBoolean(QStringView value, QString *errorMsg) +{ + if (value.compare("true"_L1, Qt::CaseInsensitive) == 0) + return true; + if (value.compare("false"_L1, Qt::CaseInsensitive) == 0) + return false; + + *errorMsg = QString::fromLatin1("Invalid value for boolean attribute: '%1'").arg(value); + return false; +} + bool RCCResourceLibrary::interpretResourceFile(QIODevice *inputDevice, const QString &fname, QString currentPath, bool listMode) { @@ -479,6 +498,7 @@ bool RCCResourceLibrary::interpretResourceFile(QIODevice *inputDevice, QLocale::Language language = QLocale::c().language(); QLocale::Territory territory = QLocale::c().territory(); QString alias; + bool empty = false; auto compressAlgo = m_compressionAlgo; int compressLevel = m_compressLevel; int compressThreshold = m_compressThreshold; @@ -538,6 +558,11 @@ bool RCCResourceLibrary::interpretResourceFile(QIODevice *inputDevice, compressThreshold = m_compressThreshold; QString errorString; + if (attributes.hasAttribute(m_strings.ATTRIBUTE_EMPTY)) + empty = parseBoolean(attributes.value(m_strings.ATTRIBUTE_EMPTY), &errorString); + else + empty = false; + if (attributes.hasAttribute(m_strings.ATTRIBUTE_COMPRESSALGO)) compressAlgo = parseCompressionAlgorithm(attributes.value(m_strings.ATTRIBUTE_COMPRESSALGO), &errorString); if (errorString.isEmpty() && attributes.hasAttribute(m_strings.ATTRIBUTE_COMPRESS)) { @@ -628,7 +653,7 @@ bool RCCResourceLibrary::interpretResourceFile(QIODevice *inputDevice, child.isDir() ? RCCFileInfo::Directory : RCCFileInfo::NoFlags, compressAlgo, compressLevel, compressThreshold, - m_noZstd)); + m_noZstd, empty)); if (!arc) m_failedResources.push_back(child.fileName()); } @@ -643,7 +668,7 @@ bool RCCResourceLibrary::interpretResourceFile(QIODevice *inputDevice, compressAlgo, compressLevel, compressThreshold, - m_noZstd) + m_noZstd, empty) ); if (!arc) m_failedResources.push_back(absFileName); diff --git a/src/tools/rcc/rcc.h b/src/tools/rcc/rcc.h index fe0e0989df..78d7600492 100644 --- a/src/tools/rcc/rcc.h +++ b/src/tools/rcc/rcc.h @@ -96,6 +96,7 @@ private: const QString ATTRIBUTE_LANG; const QString ATTRIBUTE_PREFIX; const QString ATTRIBUTE_ALIAS; + const QString ATTRIBUTE_EMPTY; const QString ATTRIBUTE_THRESHOLD; const QString ATTRIBUTE_COMPRESS; const QString ATTRIBUTE_COMPRESSALGO; diff --git a/tests/auto/corelib/io/qresourceengine/CMakeLists.txt b/tests/auto/corelib/io/qresourceengine/CMakeLists.txt index 7a4f9ec4b7..e957a4a964 100644 --- a/tests/auto/corelib/io/qresourceengine/CMakeLists.txt +++ b/tests/auto/corelib/io/qresourceengine/CMakeLists.txt @@ -31,6 +31,17 @@ qt_internal_add_test(tst_qresourceengine TESTDATA ${test_data} ) +set_source_files_properties("world.txt" + PROPERTIES QT_DISCARD_FILE_CONTENTS TRUE +) + +qt_internal_add_resource(tst_qresourceengine "qt_resource_empty" + PREFIX + "/empty" + FILES + "world.txt" +) + qt_add_resources(additional_sources testqrc/test.qrc) target_sources(tst_qresourceengine PRIVATE ${additional_sources}) diff --git a/tests/auto/corelib/io/qresourceengine/tst_qresourceengine.cpp b/tests/auto/corelib/io/qresourceengine/tst_qresourceengine.cpp index cc6d92414d..71dc45ed95 100644 --- a/tests/auto/corelib/io/qresourceengine/tst_qresourceengine.cpp +++ b/tests/auto/corelib/io/qresourceengine/tst_qresourceengine.cpp @@ -41,6 +41,7 @@ private slots: void setLocale(); void lastModified(); void resourcesInStaticPlugins(); + void qtResourceEmpty(); private: const QString m_runtimeResourceRcc; @@ -168,6 +169,7 @@ void tst_QResourceEngine::checkStructure_data() #ifdef Q_OS_ANDROID << QLatin1String("android_testdata") #endif + << QLatin1String("empty") << QLatin1String("otherdir") << QLatin1String("runtime_resource") << QLatin1String("searchpath1") @@ -620,6 +622,14 @@ void tst_QResourceEngine::resourcesInStaticPlugins() QVERIFY(QFile::exists(":/staticplugin/main.cpp")); } +void tst_QResourceEngine::qtResourceEmpty() +{ + QFile f(":/empty/world.txt"); + QVERIFY(f.exists()); + QVERIFY(f.open(QIODevice::ReadOnly)); + QVERIFY(f.readAll().isEmpty()); +} + QTEST_MAIN(tst_QResourceEngine) #include "tst_qresourceengine.moc" diff --git a/tests/auto/corelib/io/qresourceengine/world.txt b/tests/auto/corelib/io/qresourceengine/world.txt new file mode 100644 index 0000000000..ce01362503 --- /dev/null +++ b/tests/auto/corelib/io/qresourceengine/world.txt @@ -0,0 +1 @@ +hello