Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
main.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <QCoreApplication>
5#include <QCommandLineParser>
6#include <QCommandLineOption>
7#include <QDebug>
8#include <QDir>
9#include <QDirIterator>
10#include <QFile>
11#include <QFileInfo>
12#include <QHash>
13#include <QJsonDocument>
14#include <QJsonObject>
15#include <QJsonArray>
16#include <QProcess>
17#include <QStandardPaths>
18#include <QString>
19#include <QStringList>
20#include <QSet>
21#include <QElapsedTimer>
22#include <QRegularExpression>
23
24#include "../shared/depfile_shared.h"
25
26using namespace Qt::StringLiterals;
27
28// Global list of all source files that contribute to the HAP
29// Used to generate dependency file for CMake DEPFILE support
31
32struct Options
33{
38 QStringList projectLibraries; // Project-built libraries from CMake
46 QStringList pluginsImportPaths; // Build-tree plugin search paths (processed before qtPluginsDirectory)
48
49 // Qt installation directories (following androiddeployqt pattern)
50 QString qtLibsDirectory; // Target Qt libs
51 QString qtPluginsDirectory; // Target Qt plugins
52 QString qtQmlDirectory; // Target Qt QML modules
53 QString qtLibExecsDirectory; // Host Qt tools (qmlimportscanner, etc.)
54 QString qtHostDirectory; // Host Qt installation
55 QStringList extraLibsDirs; // Extra library search paths (e.g. HARMONYOS_DEPS_ROOT/lib)
56
57 bool verbose = false;
58 bool releaseMode = false;
59 bool installApk = false; // Keep name for consistency with androiddeployqt
60 bool buildPackage = true;
61
62 QString depFilePath; // Path to write dependency file
63 QString depFileBase; // Base directory for relative paths in depfile
64
65 // HarmonyOS permissions injected via qt_add_harmonyos_permission
67
68 // App-level metadata from the QT_HARMONYOS_APP_* target properties.
69 // Empty/zero means "user did not set this", so harmonydeployqt leaves the
70 // template default in place.
76
77 // SDK versions from the QT_HARMONYOS_*_SDK_VERSION target properties.
78 // Substituted into entry/build-profile.json5. Empty means "leave template
79 // default".
83
84 // Additional plugin .so files from QT_HARMONYOS_EXTRA_PLUGINS.
86
87 // Module-level metadata from the QT_HARMONYOS_MODULE_* and
88 // QT_HARMONYOS_ABILITY_* target properties.
92
93 // Test bundle mode
94 bool testBundleMode = false;
95 QString testBinariesDirectory; // Directory to scan for libtst_*.so
96 QStringList testExcludeList; // Filenames to exclude from test bundle
97
98 // HAP signing material from --signing-* CLI flags (empty = use env vars).
108
110};
111
112static void printHelp()
113{
114 fprintf(stdout, "Usage: harmonydeployqt [options]\n\n"
115 "Options:\n"
116 " --input <file> JSON configuration file (required)\n"
117 " --output <dir> Output directory for generated project\n"
118 " --hvigor <path> Path to hvigorw for building HAP (or set QT_HARMONYOS_HVIGOR)\n"
119 " --install Install HAP to connected device via hdc\n"
120 " --release Build release configuration (default: debug)\n"
121 " --verbose Enable verbose output\n"
122 " --no-build Skip building the HAP\n"
123 " --test-bundle Enable test bundle mode (bundles all test binaries into one HAP)\n"
124 " --depfile <path> Write dependency file for build system\n"
125 " --depfile-base <dir> Base directory for relative paths in depfile\n"
126 " --signing-cert-path <p> .cer file (or QT_HARMONYOS_SIGNING_CERT_PATH)\n"
127 " --signing-profile <p> .p7b profile (or QT_HARMONYOS_SIGNING_PROFILE)\n"
128 " --signing-store-file <p> .p12 keystore (or QT_HARMONYOS_SIGNING_STORE_FILE)\n"
129 " --signing-key-alias <a> Key alias (or QT_HARMONYOS_SIGNING_KEY_ALIAS)\n"
130 " --signing-key-password <s> Encrypted key pwd (or QT_HARMONYOS_SIGNING_KEY_PASSWORD)\n"
131 " --signing-store-password <s> Encrypted store pwd (or QT_HARMONYOS_SIGNING_STORE_PASSWORD)\n"
132 " --signing-plain-key-password <s>\n"
133 " Plain key pwd (or QT_HARMONYOS_SIGNING_PLAIN_KEY_PASSWORD)\n"
134 " --signing-plain-store-password <s>\n"
135 " Plain store pwd (or QT_HARMONYOS_SIGNING_PLAIN_STORE_PASSWORD)\n"
136 " --signing-alg <alg> Signature algorithm, default SHA256withECDSA\n"
137 " (or QT_HARMONYOS_SIGNING_ALG)\n"
138 " --help Show this help\n\n"
139 "Signing: CLI flags above win per field over the matching env vars.\n"
140 "Both passwords must be given either hvigor-encrypted or in plain text.\n"
141 "If any signing input is set, all required values must be present,\n"
142 "otherwise harmonydeployqt fails. With none set the HAP is left unsigned.\n"
143 "With encrypted passwords, hvigor signs the HAP. With plain passwords, the\n"
144 "SDK's hap-sign-tool does, located via the SDK root or\n"
145 "QT_HARMONYOS_HAP_SIGN_TOOL and run with QT_HARMONYOS_JAVA, JAVA_HOME or\n"
146 "java from PATH.\n");
147}
148
149class QProcessExt : public QProcess
150{
151public:
153 connect(this, &QProcess::readyReadStandardOutput, [this]() {
154 QByteArray output = readAllStandardOutput();
155 QString text = QString::fromUtf8(output);
156 fprintf(stderr, "harmonydeployqt: external application output: %s\n", qPrintable(text));
157 });
158 connect(this, &QProcess::readyReadStandardError, [this]() {
159 QByteArray error = readAllStandardError();
160 QString text = QString::fromUtf8(error);
161 fprintf(stderr, "harmonydeployqt: external application error: %s\n", qPrintable(text));
162 });
163 }
164};
165
166static bool parseCommandLine(const QStringList &arguments, Options *options)
167{
168 QCommandLineParser parser;
169 parser.setApplicationDescription("Qt HarmonyOS Deployment Tool"_L1);
170
171 QCommandLineOption inputOption("input"_L1, "JSON configuration file"_L1, "file"_L1);
172 QCommandLineOption outputOption("output"_L1, "Output directory"_L1, "dir"_L1);
173 QCommandLineOption hvigorOption("hvigor"_L1, "Path to hvigorw"_L1, "path"_L1);
174 QCommandLineOption installOption("install"_L1, "Install to device"_L1);
175 QCommandLineOption releaseOption("release"_L1, "Build release configuration"_L1);
176 QCommandLineOption verboseOption("verbose"_L1, "Verbose output"_L1);
177 QCommandLineOption noBuildOption("no-build"_L1, "Skip building"_L1);
178 QCommandLineOption testBundleOption("test-bundle"_L1, "Enable test bundle mode"_L1);
179 QCommandLineOption depfileOption("depfile"_L1, "Dependency file output"_L1, "path"_L1);
180 QCommandLineOption depfileBaseOption("depfile-base"_L1, "Base directory for depfile paths"_L1, "dir"_L1);
181 QCommandLineOption signingCertPathOption("signing-cert-path"_L1,
182 "Path to the .cer file"_L1, "path"_L1);
183 QCommandLineOption signingProfileOption("signing-profile"_L1,
184 "Path to the .p7b profile"_L1, "path"_L1);
185 QCommandLineOption signingStoreFileOption("signing-store-file"_L1,
186 "Path to the .p12 keystore"_L1, "path"_L1);
187 QCommandLineOption signingKeyAliasOption("signing-key-alias"_L1,
188 "Key alias inside the keystore"_L1, "alias"_L1);
189 QCommandLineOption signingKeyPasswordOption("signing-key-password"_L1,
190 "Encrypted key password"_L1, "pwd"_L1);
191 QCommandLineOption signingStorePasswordOption("signing-store-password"_L1,
192 "Encrypted keystore password"_L1, "pwd"_L1);
193 QCommandLineOption signingPlainKeyPasswordOption("signing-plain-key-password"_L1,
194 "Plain text key password"_L1, "pwd"_L1);
195 QCommandLineOption signingPlainStorePasswordOption("signing-plain-store-password"_L1,
196 "Plain text keystore password"_L1, "pwd"_L1);
197 QCommandLineOption signingAlgOption("signing-alg"_L1,
198 "Signature algorithm (default SHA256withECDSA)"_L1, "alg"_L1);
199 QCommandLineOption helpOption("help"_L1, "Show help"_L1);
200
201 parser.addOption(inputOption);
202 parser.addOption(outputOption);
203 parser.addOption(hvigorOption);
204 parser.addOption(installOption);
205 parser.addOption(releaseOption);
206 parser.addOption(verboseOption);
207 parser.addOption(noBuildOption);
208 parser.addOption(testBundleOption);
209 parser.addOption(depfileOption);
210 parser.addOption(depfileBaseOption);
211 parser.addOption(signingCertPathOption);
212 parser.addOption(signingProfileOption);
213 parser.addOption(signingStoreFileOption);
214 parser.addOption(signingKeyAliasOption);
215 parser.addOption(signingKeyPasswordOption);
216 parser.addOption(signingStorePasswordOption);
217 parser.addOption(signingPlainKeyPasswordOption);
218 parser.addOption(signingPlainStorePasswordOption);
219 parser.addOption(signingAlgOption);
220 parser.addOption(helpOption);
221
222 if (!parser.parse(arguments)) {
223 fprintf(stderr, "%s\n", qPrintable(parser.errorText()));
224 return false;
225 }
226
227 if (parser.isSet(helpOption)) {
229 return false;
230 }
231
232 if (!parser.isSet(inputOption)) {
233 fprintf(stderr, "Error: --input option is required\n");
235 return false;
236 }
237
238 options->inputFile = parser.value(inputOption);
239 options->outputDirectory = parser.value(outputOption);
240 options->hvigorPath = parser.value(hvigorOption);
241 options->installApk = parser.isSet(installOption);
242 options->releaseMode = parser.isSet(releaseOption);
243 options->verbose = parser.isSet(verboseOption);
244 options->buildPackage = !parser.isSet(noBuildOption);
245 options->testBundleMode = parser.isSet(testBundleOption);
246 options->depFilePath = parser.value(depfileOption);
247 options->depFileBase = parser.value(depfileBaseOption);
248 options->signingCertPath = parser.value(signingCertPathOption);
249 options->signingProfile = parser.value(signingProfileOption);
250 options->signingStoreFile = parser.value(signingStoreFileOption);
251 options->signingKeyAlias = parser.value(signingKeyAliasOption);
252 options->signingKeyPassword = parser.value(signingKeyPasswordOption);
253 options->signingStorePassword = parser.value(signingStorePasswordOption);
254 options->signingPlainKeyPassword = parser.value(signingPlainKeyPasswordOption);
255 options->signingPlainStorePassword = parser.value(signingPlainStorePasswordOption);
256 options->signingAlg = parser.value(signingAlgOption);
257
258 return true;
259}
260
261// Returns the version of the SDK that hvigor builds with, in build-profile.json5
262// format, for example "6.1.1(24)", or an empty string if it cannot be determined.
263// hvigor uses DEVECO_SDK_HOME; without it, assume the SDK Qt was built with.
264static QString sdkVersion(const QString &sdkRoot)
265{
266 QStringList candidates;
267 const QString devEcoSdkHome = qEnvironmentVariable("DEVECO_SDK_HOME");
268 if (!devEcoSdkHome.isEmpty())
269 candidates << devEcoSdkHome + "/default/sdk-pkg.json"_L1;
270 if (!sdkRoot.isEmpty())
271 candidates << sdkRoot + "/../sdk-pkg.json"_L1;
272 for (const QString &candidate : std::as_const(candidates)) {
273 QFile file(candidate);
274 if (!file.open(QIODevice::ReadOnly))
275 continue;
276 const QJsonObject data = QJsonDocument::fromJson(file.readAll())["data"_L1].toObject();
277 const QString platformVersion = data["platformVersion"_L1].toString();
278 const QString apiVersion = data["apiVersion"_L1].toVariant().toString();
279 if (platformVersion.isEmpty() || apiVersion.isEmpty())
280 return {};
281 return platformVersion + u'(' + apiVersion + u')';
282 }
283 return {};
284}
285
286static bool readInputConfiguration(Options *options)
287{
288 QFile inputFile(options->inputFile);
289 if (!inputFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
290 fprintf(stderr, "Failed to open input file: %s\n", qPrintable(options->inputFile));
291 return false;
292 }
293
294 QJsonParseError parseError;
295 QJsonDocument doc = QJsonDocument::fromJson(inputFile.readAll(), &parseError);
296 if (doc.isNull()) {
297 fprintf(stderr, "Failed to parse JSON: %s\n", qPrintable(parseError.errorString()));
298 return false;
299 }
300
301 QJsonObject obj = doc.object();
302
303 options->applicationBinary = obj["application-binary"_L1].toString();
304 options->harmonyOsPackageSourceDirectory = obj["harmonyos-package-source-directory"_L1].toString();
305 options->harmonyOsAppName = obj["harmonyos-app-name"_L1].toString();
306 options->harmonyOsAppBundleName = obj["harmonyos-app-bundle-name"_L1].toString();
307 options->sdkRoot = obj["sdk-root"_L1].toString();
308 options->ndkRoot = obj["ndk-root"_L1].toString();
309 // qml-root-path may be a string (legacy) or an array (current).
310 {
311 const QJsonValue rootPathValue = obj["qml-root-path"_L1];
312 if (rootPathValue.isArray()) {
313 for (const QJsonValue &v : rootPathValue.toArray()) {
314 const QString s = v.toString();
315 if (!s.isEmpty())
316 options->qmlRootPaths.append(s);
317 }
318 } else {
319 const QString s = rootPathValue.toString();
320 if (!s.isEmpty())
321 options->qmlRootPaths.append(s);
322 }
323 }
324
325 // Qt installation directories
326 options->qtLibsDirectory = obj["qtLibsDirectory"_L1].toString();
327 options->qtPluginsDirectory = obj["qtPluginsDirectory"_L1].toString();
328 options->qtQmlDirectory = obj["qtQmlDirectory"_L1].toString();
329 options->qtLibExecsDirectory = obj["qtLibExecsDirectory"_L1].toString();
330 options->qtHostDirectory = obj["qtHostDirectory"_L1].toString();
331
332 QJsonArray extraLibsDirsArray = obj["extra-libs-dirs"_L1].toArray();
333 for (const QJsonValue &value : extraLibsDirsArray)
334 options->extraLibsDirs.append(value.toString());
335
336 // Test bundle mode settings (JSON can override CLI flag)
337 if (obj["test-bundle"_L1].toBool())
338 options->testBundleMode = true;
339 options->testBinariesDirectory = obj["test-binaries-directory"_L1].toString();
340 QJsonArray excludeArray = obj["test-exclude-list"_L1].toArray();
341 for (const QJsonValue &value : excludeArray)
342 options->testExcludeList.append(value.toString());
343
344 // Parse project libraries
345 QJsonArray projectLibsArray = obj["project-libraries"_L1].toArray();
346 for (const QJsonValue &value : projectLibsArray)
347 options->projectLibraries.append(value.toString());
348
349 // Parse QML import paths
350 QJsonArray importPathsArray = obj["qml-import-paths"_L1].toArray();
351 for (const QJsonValue &value : importPathsArray)
352 options->qmlImportPaths.append(value.toString());
353
354 // Parse plugins import paths
355 QJsonArray pluginsImportPathsArray = obj["plugins-import-paths"_L1].toArray();
356 for (const QJsonValue &value : pluginsImportPathsArray)
357 options->pluginsImportPaths.append(value.toString());
358
359 // Parse target architectures
360 QJsonArray archArray = obj["harmonyos-target-arch"_L1].toArray();
361 for (const QJsonValue &value : archArray)
362 options->targetArchs.append(value.toString());
363 if (options->targetArchs.isEmpty())
364 options->targetArchs.append("arm64-v8a"_L1);
365
366 // Parse HarmonyOS permissions injected via qt_add_harmonyos_permission
367 options->permissions = obj["permissions"_L1].toArray();
368
369 // App-level metadata injected via the QT_HARMONYOS_APP_* target properties.
370 options->harmonyOsAppVendor = obj["harmonyos-app-vendor"_L1].toString();
371 options->harmonyOsAppVersionCode = obj["harmonyos-app-version-code"_L1].toInt();
372 options->harmonyOsAppVersionName = obj["harmonyos-app-version-name"_L1].toString();
373 options->harmonyOsAppLabel = obj["harmonyos-app-label"_L1].toString();
374 options->harmonyOsAppIcon = obj["harmonyos-app-icon"_L1].toString();
375
376 // SDK versions for entry/build-profile.json5.
377 options->harmonyOsCompatibleSdkVersion =
378 obj["harmonyos-compatible-sdk-version"_L1].toString();
379 options->harmonyOsTargetSdkVersion =
380 obj["harmonyos-target-sdk-version"_L1].toString();
381 options->harmonyOsCompileSdkVersion =
382 obj["harmonyos-compile-sdk-version"_L1].toString();
383 // DevEco Studio asks to add a targetSdkVersion matching the SDK when it is missing.
384 // With an explicit compileSdkVersion the SDK version could exceed it.
385 if (options->harmonyOsTargetSdkVersion.isEmpty()
386 && options->harmonyOsCompileSdkVersion.isEmpty()) {
387 options->harmonyOsTargetSdkVersion = sdkVersion(options->sdkRoot);
388 }
389
390 // Extra plugins (resolved file paths). Categories are derived from the
391 // parent directory name of each path.
392 {
393 const QJsonArray extraPluginsArray =
394 obj["harmonyos-extra-plugins"_L1].toArray();
395 for (const QJsonValue &v : extraPluginsArray) {
396 const QString s = v.toString();
397 if (!s.isEmpty())
398 options->extraPlugins.append(s);
399 }
400 }
401
402 // Module-level metadata injected via the QT_HARMONYOS_MODULE_* and
403 // QT_HARMONYOS_ABILITY_* target properties.
404 options->harmonyOsModuleDescription = obj["harmonyos-module-description"_L1].toString();
405 const QJsonArray deviceTypesArray = obj["harmonyos-module-device-types"_L1].toArray();
406 for (const QJsonValue &value : deviceTypesArray)
407 options->harmonyOsModuleDeviceTypes.append(value.toString());
408 options->harmonyOsAbilityOrientation = obj["harmonyos-ability-orientation"_L1].toString();
409
410 // Validate required fields
411 if (!options->testBundleMode) {
412 if (options->applicationBinary.isEmpty()) {
413 fprintf(stderr, "Error: 'application-binary' not specified in JSON\n");
414 return false;
415 }
416
417 // The settings file is generated at CMake generate time, so it names the
418 // application binary long before the build produces it. Fail early with a
419 // clear reason instead of copying the whole template first and then
420 // tripping over the missing file in copyApplicationBinary().
421 if (!QFile::exists(options->applicationBinary)) {
422 fprintf(stderr, "Error: application binary does not exist: %s\n",
423 qPrintable(options->applicationBinary));
424 fprintf(stderr, " Build the project before running harmonydeployqt.\n");
425 return false;
426 }
427 }
428
429 // Set defaults for test bundle mode
430 if (options->testBundleMode) {
431 if (options->harmonyOsAppBundleName.isEmpty())
432 options->harmonyOsAppBundleName = "org.qtproject.autotests"_L1;
433 if (options->harmonyOsAppName.isEmpty())
434 options->harmonyOsAppName = "QtAutoTests"_L1;
435 }
436
437 // Auto-detect template directory if not specified
438 if (options->harmonyOsPackageSourceDirectory.isEmpty()) {
439 // For test bundle mode, use qtLibsDirectory as starting point;
440 // otherwise start from the application binary location
441 QString searchPath;
442 if (options->testBundleMode && !options->qtLibsDirectory.isEmpty()) {
443 searchPath = QDir::cleanPath(options->qtLibsDirectory);
444 } else if (!options->applicationBinary.isEmpty()) {
445 QFileInfo appInfo(options->applicationBinary);
446 searchPath = QDir::cleanPath(appInfo.absolutePath());
447 }
448
449 if (searchPath.isEmpty()) {
450 fprintf(stderr, "Error: 'harmonyos-package-source-directory' not specified in JSON\n");
451 fprintf(stderr, " and could not auto-detect template location (no search path)\n");
452 return false;
453 }
454
455 if (options->verbose)
456 fprintf(stdout, "Searching for template starting from: %s\n", qPrintable(searchPath));
457
458 // Walk up directory tree to find Qt installation using string manipulation
459 for (int i = 0; i < 10; ++i) {
460 // Check for installed template in share directory (matches CMakeLists.txt install path)
461 QString templatePath = searchPath + "/share/qt6/src/harmonyos/templates"_L1;
462 if (options->verbose) {
463 fprintf(stdout, " Checking: %s ... %s\n", qPrintable(templatePath),
464 QDir(templatePath).exists() ? "FOUND" : "not found");
465 }
466 if (QDir(templatePath).exists()) {
467 options->harmonyOsPackageSourceDirectory = std::move(templatePath);
468 break;
469 }
470
471 // Check for source tree location (development builds)
472 templatePath = searchPath + "/src/harmonyos/templates"_L1;
473 if (options->verbose) {
474 fprintf(stdout, " Checking: %s ... %s\n", qPrintable(templatePath),
475 QDir(templatePath).exists() ? "FOUND" : "not found");
476 }
477 if (QDir(templatePath).exists()) {
478 options->harmonyOsPackageSourceDirectory = std::move(templatePath);
479 break;
480 }
481
482 // Move up one directory by removing last path component
483 const auto lastSlash = searchPath.lastIndexOf('/'_L1);
484 if (lastSlash <= 0) {
485 if (options->verbose)
486 fprintf(stdout, " Reached root directory\n");
487 break;
488 }
489 searchPath.resize(lastSlash);
490 }
491
492 if (options->harmonyOsPackageSourceDirectory.isEmpty()) {
493 fprintf(stderr, "Error: 'harmonyos-package-source-directory' not specified in JSON\n");
494 fprintf(stderr, " and could not auto-detect template location\n");
495 fprintf(stderr, " Please specify the path to the HarmonyOS application template\n");
496 return false;
497 } else if (options->verbose) {
498 fprintf(stdout, "Auto-detected template: %s\n", qPrintable(options->harmonyOsPackageSourceDirectory));
499 }
500 }
501
502 if (options->harmonyOsAppName.isEmpty()) {
503 fprintf(stderr, "Error: 'harmonyos-app-name' not specified in JSON\n");
504 return false;
505 }
506
507 if (options->harmonyOsAppBundleName.isEmpty()) {
508 fprintf(stderr, "Error: 'harmonyos-app-bundle-name' not specified in JSON\n");
509 return false;
510 }
511
512 // Set default output directory if not specified
513 if (options->outputDirectory.isEmpty()) {
514 if (options->testBundleMode) {
515 options->outputDirectory = QDir::currentPath() + "/harmonyos-tests-bundle"_L1;
516 } else {
517 QFileInfo appInfo(options->applicationBinary);
518 options->outputDirectory = QDir::currentPath() + "/"_L1 +
519 appInfo.completeBaseName() + "-harmonyos"_L1;
520 }
521 }
522
523 if (options->verbose) {
524 fprintf(stdout, "Configuration loaded:\n");
525 if (options->testBundleMode) {
526 fprintf(stdout, " Mode: test bundle\n");
527 fprintf(stdout, " Test binaries directory: %s\n", qPrintable(options->testBinariesDirectory));
528 if (!options->testExcludeList.isEmpty())
529 fprintf(stdout, " Exclude list: %s\n", qPrintable(options->testExcludeList.join(", "_L1)));
530 } else {
531 fprintf(stdout, " Application binary: %s\n", qPrintable(options->applicationBinary));
532 }
533 fprintf(stdout, " Template directory: %s\n", qPrintable(options->harmonyOsPackageSourceDirectory));
534 fprintf(stdout, " App name: %s\n", qPrintable(options->harmonyOsAppName));
535 fprintf(stdout, " Bundle name: %s\n", qPrintable(options->harmonyOsAppBundleName));
536 fprintf(stdout, " Output directory: %s\n", qPrintable(options->outputDirectory));
537 fprintf(stdout, " Target architectures: %s\n", qPrintable(options->targetArchs.join(", "_L1)));
538 }
539
540 return true;
541}
542
543static bool copyFileIfNewer(const QString &sourceFileName,
544 const QString &destinationFileName, bool verbose,
545 bool forceOverwrite = false)
546{
547 if (QFile::exists(destinationFileName)) {
548 QFileInfo destinationFileInfo(destinationFileName);
549 QFileInfo sourceFileInfo(sourceFileName);
550
551 // Skip if destination is same or newer (unless forcing overwrite)
552 if (!forceOverwrite &&
553 sourceFileInfo.lastModified() <= destinationFileInfo.lastModified()) {
554 if (verbose)
555 fprintf(stdout, " Skipping: %s (destination is up to date)\n",
556 qPrintable(sourceFileInfo.fileName()));
557 return true;
558 }
559
560 // Remove old file before copying
561 if (!QFile(destinationFileName).remove()) {
562 fprintf(stderr, "Failed to remove old file: %s\n",
563 qPrintable(destinationFileName));
564 return false;
565 }
566 }
567
568 // Ensure destination directory exists
569 QFileInfo destInfo(destinationFileName);
570 if (!QDir().mkpath(destInfo.absolutePath())) {
571 fprintf(stderr, "Failed to create directory for: %s\n", qPrintable(destinationFileName));
572 return false;
573 }
574
575 if (verbose)
576 fprintf(stdout, " Copying: %s\n", qPrintable(QFileInfo(sourceFileName).fileName()));
577
578 QFile sourceFile(sourceFileName);
579 if (!sourceFile.copy(destinationFileName)) {
580 fprintf(stderr, "Failed to copy file: %s to %s: %s\n", qPrintable(sourceFileName),
581 qPrintable(destinationFileName), qPrintable(sourceFile.errorString()));
582 return false;
583 }
584
585 return true;
586}
587
588// Write dependency file for CMake DEPFILE support
589static bool writeDepfile(const Options &options, const QString &hapOutputPath)
590{
591 if (options.depFilePath.isEmpty())
592 return true; // Not requested
593
594 if (options.verbose)
595 fprintf(stdout, "Writing dependency file: %s\n", qPrintable(options.depFilePath));
596
597 // Calculate relative HAP path from depfile base directory
598 QString relativeHapPath;
599 if (!options.depFileBase.isEmpty() && !hapOutputPath.isEmpty())
600 relativeHapPath = QDir(options.depFileBase).relativeFilePath(hapOutputPath);
601 else
602 relativeHapPath = hapOutputPath;
603
604 // Open depfile for writing
605 QFile depFile(options.depFilePath);
606 if (!depFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
607 fprintf(stderr, "Failed to open depfile: %s\n", qPrintable(options.depFilePath));
608 return false;
609 }
610
611 // Write Makefile-style dependency format: target: dep1 \ dep2 \ ...
612 depFile.write(escapeAndEncodeDependencyPath(relativeHapPath));
613 depFile.write(": ");
614
615 for (const QString &dep : dependenciesForDepfile) {
616 depFile.write(" \\\n ");
617 depFile.write(escapeAndEncodeDependencyPath(dep));
618 }
619
620 depFile.write("\n");
621 depFile.close();
622
623 if (options.verbose)
624 fprintf(stdout, "Wrote %lld dependencies to depfile\n",
625 static_cast<long long>(dependenciesForDepfile.size()));
626
627 return true;
628}
629
630static bool copyRecursively(const QString &sourceDir, const QString &destDir, bool verbose,
631 const QString &topDest = {})
632{
633 QDir srcDir(sourceDir);
634 if (!srcDir.exists()) {
635 fprintf(stderr, "Source directory does not exist: %s\n", qPrintable(sourceDir));
636 return false;
637 }
638
639 QDir destDirectory(destDir);
640 if (!destDirectory.exists()) {
641 if (!destDirectory.mkpath("."_L1)) {
642 fprintf(stderr, "Failed to create destination directory: %s\n", qPrintable(destDir));
643 return false;
644 }
645 }
646
647 // Fixed top-level destination so the check below matches the output dir at any depth.
648 const QFileInfo cleanDestInfo{ topDest.isEmpty()
649 ? QDir::cleanPath(destDirectory.absolutePath())
650 : topDest };
651
652 const QFileInfoList entries = srcDir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden);
653 for (const QFileInfo &entry : entries) {
654 QString destPath = destDir + "/"_L1 + entry.fileName();
655
656 if (entry.isDir()) {
657 // Skip the destination itself; otherwise the copy descends into its own output.
658 const QFileInfo cleanSrcInfo{ QDir::cleanPath(entry.absoluteFilePath()) };
659 if (cleanSrcInfo == cleanDestInfo) {
660 if (verbose)
661 fprintf(stdout, " Skipping %s (is the destination directory)\n",
662 qPrintable(cleanSrcInfo.filePath()));
663 continue;
664 }
665 if (!copyRecursively(entry.filePath(), destPath, verbose, cleanDestInfo.filePath()))
666 return false;
667 } else {
668 if (!copyFileIfNewer(entry.filePath(), destPath, verbose))
669 return false;
670 }
671 }
672 return true;
673}
674
675static bool copyTemplate(const Options &options)
676{
677 if (options.verbose) {
678 fprintf(stdout, "Copying template from %s to %s\n",
679 qPrintable(options.harmonyOsPackageSourceDirectory),
680 qPrintable(options.outputDirectory));
681 }
682
683 // Check if template exists
684 QDir templateDir(options.harmonyOsPackageSourceDirectory);
685 if (!templateDir.exists()) {
686 fprintf(stderr, "Template directory does not exist: %s\n",
687 qPrintable(options.harmonyOsPackageSourceDirectory));
688 return false;
689 }
690
691 // The module manifest is mandatory; without it hvigor cannot build the HAP and
692 // customizeTemplate() would silently skip every substitution, producing a broken
693 // package. Fail early with a targeted message instead.
694 const QString moduleManifest =
695 options.harmonyOsPackageSourceDirectory + "/entry/src/main/module.json5"_L1;
696 if (!QFile::exists(moduleManifest)) {
697 fprintf(stderr,
698 "'%s' is not a valid HarmonyOS package template: missing "
699 "entry/src/main/module.json5\n",
700 qPrintable(options.harmonyOsPackageSourceDirectory));
701 return false;
702 }
703
704 // Create output directory
705 QDir outputDir(options.outputDirectory);
706 if (outputDir.exists()) {
707 if (options.verbose)
708 fprintf(stdout, "Output directory already exists, will overwrite files\n");
709 }
710
711 // Copy entire template
712 if (!copyRecursively(options.harmonyOsPackageSourceDirectory, options.outputDirectory, options.verbose))
713 return false;
714
715 // Force-overwrite the manifest files. customizeTemplate() does one-shot
716 // sentinel/regex substitutions on these; if the destination is left over
717 // from a previous deploy the sentinels are already gone and any change to
718 // the CMake-supplied metadata would be silently ignored. The same applies
719 // to build-profile.json5: injectSigningConfig() looks for the empty
720 // template array and refuses to touch anything else.
721 for (const char *relPath : { "AppScope/app.json5", "entry/src/main/module.json5",
722 "build-profile.json5" }) {
723 const QString src = options.harmonyOsPackageSourceDirectory
724 + QLatin1Char('/') + QLatin1String(relPath);
725 const QString dst = options.outputDirectory + QLatin1Char('/') + QLatin1String(relPath);
726 if (QFile::exists(src) && !copyFileIfNewer(src, dst, options.verbose, true))
727 return false;
728 }
729
730 if (options.verbose)
731 fprintf(stdout, "Template copied successfully\n");
732
733 return true;
734}
735
736// Hvigor's module.json5 schema rejects free-form reason strings: it requires
737// either a "$string:<id>" resource reference or a parameterised token (one
738// containing both '{' and '}'). Plain English literals supplied via
739// qt_add_harmonyos_permission(... REASON "...") therefore have to be
740// auto-promoted to a synthesized resource entry before substitution.
741static bool reasonNeedsPromotion(const QString &reason)
742{
743 if (reason.startsWith("$string:"_L1))
744 return false;
745 if (reason.contains(QLatin1Char('{')) && reason.contains(QLatin1Char('}')))
746 return false;
747 return true;
748}
749
750// Synthesize a stable resource id from a permission name. The trailing
751// dot-separated component is unique among ohos.permission.* permissions, so
752// "ohos.permission.CAMERA" -> "qt_permission_reason_camera".
753static QString synthesizePermissionReasonId(const QString &permissionName)
754{
755 const QString suffix = permissionName.section(QLatin1Char('.'), -1).toLower();
756 return "qt_permission_reason_"_L1 + suffix;
757}
758
759// scalar. User-supplied metadata (vendor, label, etc.) is substituted into
760// the OHOS manifest files verbatim, so embedded '"' or '\' would otherwise
761// produce invalid JSON that hvigor rejects. Re-uses Qt's own JSON writer
762// for the canonical escape: wrap in a single-element array, serialize, strip
763// the surrounding `["` and `"]`.
764static QString jsonStringEscape(const QString &s)
765{
766 QByteArray ba = QJsonDocument(QJsonArray{s}).toJson(QJsonDocument::Compact);
767 return QString::fromUtf8(ba.sliced(2, ba.size() - 4));
768}
769
770// Returns true when value is one of the orientation strings the HarmonyOS
771// module.json5 schema accepts. Anything else gets rejected with a warning and
772// dropped, so a typo never reaches hvigor (which would fail with a less
773// targeted schema error).
774static bool isValidHarmonyOsAbilityOrientation(const QString &value)
775{
776 static const QStringList allowed = {
777 "unspecified"_L1,
778 "landscape"_L1,
779 "portrait"_L1,
780 "follow_recent"_L1,
781 "landscape_inverted"_L1,
782 "portrait_inverted"_L1,
783 "auto_rotation"_L1,
784 "auto_rotation_landscape"_L1,
785 "auto_rotation_portrait"_L1,
786 "auto_rotation_restricted"_L1,
787 "auto_rotation_landscape_restricted"_L1,
788 "auto_rotation_portrait_restricted"_L1,
789 "locked"_L1,
790 "follow_desktop"_L1,
791 };
792 return allowed.contains(value);
793}
794
800
801static bool customizeTemplate(const Options &options)
802{
803 if (options.verbose)
804 fprintf(stdout, "Customizing template files\n");
805
806 // Customize QtAppConstants.ets
807 QString qtAppConstantsPath = options.outputDirectory + "/entry/src/main/ets/common/QtAppConstants.ets"_L1;
808 QFile qtAppConstantsFile(qtAppConstantsPath);
809
810 if (qtAppConstantsFile.exists()) {
811 if (!qtAppConstantsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
812 fprintf(stderr, "Failed to open QtAppConstants.ets for reading\n");
813 return false;
814 }
815
816 QString content = QString::fromUtf8(qtAppConstantsFile.readAll());
817 qtAppConstantsFile.close();
818
819 // Replace APP_LIBRARY_NAME
820 // In test bundle mode, use a placeholder — runtime override selects the actual test
821 QString appLibName;
822 if (options.testBundleMode) {
823 appLibName = "libtst_placeholder.so"_L1;
824 } else {
825 QFileInfo appInfo(options.applicationBinary);
826 appLibName = appInfo.fileName(); // Keep the full filename with lib prefix and .so extension
827 }
828
829 content.replace(QRegularExpression("APP_LIBRARY_NAME = '[^']*'"_L1),
830 "APP_LIBRARY_NAME = '"_L1 + appLibName + "'"_L1);
831
832 if (!qtAppConstantsFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
833 fprintf(stderr, "Failed to open QtAppConstants.ets for writing\n");
834 return false;
835 }
836
837 qtAppConstantsFile.write(content.toUtf8());
838 qtAppConstantsFile.close();
839
840 if (options.verbose)
841 fprintf(stdout, " Updated QtAppConstants.ets with app name: %s\n", qPrintable(appLibName));
842 }
843
844 // Resolve the icon value once -- it is consumed by both the app.json5
845 // (app-level icon) and module.json5 (ability/launcher icon) customizations
846 // below. $media: references pass through; literal filesystem paths get
847 // copied into the AppScope *and* entry resource dirs and rewritten to a
848 // $media:<basename> reference. OHOS restool restricts resource names to
849 // [a-zA-Z0-9_]; sanitize the basename so files like "qt-logo.png" are
850 // accepted (becomes "qt_logo.png" / $media:qt_logo).
851 QString iconValue = options.harmonyOsAppIcon;
852 if (!iconValue.isEmpty() && !iconValue.startsWith("$media:"_L1)) {
853 QFileInfo iconInfo(iconValue);
854 if (!iconInfo.exists() || !iconInfo.isFile()) {
855 fprintf(stderr, "App icon does not exist: %s\n", qPrintable(iconValue));
856 return false;
857 }
858 QString safeStem = iconInfo.completeBaseName();
859 for (QChar &c : safeStem) {
860 if (!c.isLetterOrNumber() && c != QLatin1Char('_'))
861 c = QLatin1Char('_');
862 }
863 const QString destFileName = iconInfo.suffix().isEmpty()
864 ? safeStem
865 : safeStem + "."_L1 + iconInfo.suffix();
866 const QStringList destDirs = {
867 options.outputDirectory + "/AppScope/resources/base/media"_L1,
868 options.outputDirectory + "/entry/src/main/resources/base/media"_L1,
869 };
870 for (const QString &destDir : destDirs) {
871 QDir().mkpath(destDir);
872 const QString destPath = destDir + "/"_L1 + destFileName;
873 if (!copyFileIfNewer(iconValue, destPath, options.verbose)) {
874 fprintf(stderr, "Failed to copy app icon to: %s\n", qPrintable(destPath));
875 return false;
876 }
877 }
878 iconValue = "$media:"_L1 + safeStem;
879 }
880
881 // Customize AppScope/app.json5. Use targeted text substitution rather
882 // than parse-and-rewrite so JSON5 features in the template (comments,
883 // trailing commas, single-quoted strings) survive the round trip --
884 // QJsonDocument is a strict JSON parser and would reject those.
885 QString appJsonPath = options.outputDirectory + "/AppScope/app.json5"_L1;
886 QFile appJsonFile(appJsonPath);
887
888 if (appJsonFile.exists()) {
889 if (!appJsonFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
890 fprintf(stderr, "Failed to open app.json5 for reading\n");
891 return false;
892 }
893 QString content = QString::fromUtf8(appJsonFile.readAll());
894 appJsonFile.close();
895
896 auto replaceStringField =
897 [&content](QLatin1StringView key, const QString &value) {
898 if (value.isEmpty())
899 return;
900 content.replace(
901 QRegularExpression("\""_L1 + key + "\":\\s*\"[^\"]*\""_L1),
902 "\""_L1 + key + "\": \""_L1 + jsonStringEscape(value) + "\""_L1);
903 };
904
905 replaceStringField("bundleName"_L1, options.harmonyOsAppBundleName);
906 replaceStringField("vendor"_L1, options.harmonyOsAppVendor);
907 replaceStringField("versionName"_L1, options.harmonyOsAppVersionName);
908 // The OHOS schema for app.label requires either "$string:<id>" or a
909 // brace-substituted value -- a plain literal is rejected. So only
910 // substitute the label field directly when the user supplied a
911 // $string: reference. Literal labels are routed below to the
912 // app_name/QAbility_label string resources, which app.json5 and
913 // module.json5 already reference via $string:.
914 if (options.harmonyOsAppLabel.startsWith("$string:"_L1))
915 replaceStringField("label"_L1, options.harmonyOsAppLabel);
916 replaceStringField("icon"_L1, iconValue);
917
918 if (options.harmonyOsAppVersionCode > 0) {
919 content.replace(
920 QRegularExpression("\"versionCode\":\\s*\\d+"_L1),
921 "\"versionCode\": "_L1 + QString::number(options.harmonyOsAppVersionCode));
922 }
923
924 if (!appJsonFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
925 fprintf(stderr, "Failed to open app.json5 for writing\n");
926 return false;
927 }
928 appJsonFile.write(content.toUtf8());
929 appJsonFile.close();
930
931 if (options.verbose) {
932 fprintf(stdout, " Updated app.json5 (bundle: %s)\n",
933 qPrintable(options.harmonyOsAppBundleName));
934 }
935 }
936
937 // Display label that lands in the app_name and QAbility_label string
938 // resources. A literal LABEL wins; a "$string:..." LABEL was substituted
939 // into app.json5 above and is therefore the user's own resource id, so we
940 // fall back to the target name here.
941 const QString displayLabel =
942 (!options.harmonyOsAppLabel.isEmpty()
943 && !options.harmonyOsAppLabel.startsWith("$string:"_L1))
944 ? options.harmonyOsAppLabel
945 : options.harmonyOsAppName;
946
947 // Auto-promote plain-literal permission reasons to $string: references.
948 // The literal values are appended to the entry/.../string.json files below
949 // so the synthesized resource ids resolve correctly at HAP build time.
950 QJsonArray transformedPermissions;
951 QList<PromotedReason> promotedReasons;
952 QSet<QString> seenPromotedIds;
953 for (const QJsonValue &value : std::as_const(options.permissions)) {
954 if (!value.isObject()) {
955 transformedPermissions.append(value);
956 continue;
957 }
958 QJsonObject entry = value.toObject();
959 if (entry.contains("reason"_L1)) {
960 const QString reason = entry["reason"_L1].toString();
961 if (reasonNeedsPromotion(reason)) {
962 const QString permName = entry["name"_L1].toString();
963 const QString stringId = synthesizePermissionReasonId(permName);
964 entry["reason"_L1] = QString("$string:"_L1 + stringId);
965 if (!seenPromotedIds.contains(stringId)) {
966 promotedReasons.append({stringId, reason});
967 seenPromotedIds.insert(stringId);
968 }
969 }
970 }
971 transformedPermissions.append(entry);
972 }
973
974 // Customize entry module string resources: replace QAbility_label with the
975 // app name, and append any synthesized permission-reason strings.
976 // Update all locale variants: base, en_US, zh_CN
977 QStringList locales = QStringList() << "base"_L1 << "en_US"_L1 << "zh_CN"_L1;
978
979 for (const QString &locale : locales) {
980 QString stringJsonPath = options.outputDirectory + "/entry/src/main/resources/"_L1 +
981 locale + "/element/string.json"_L1;
982 QFile stringJsonFile(stringJsonPath);
983
984 if (!stringJsonFile.exists())
985 continue;
986
987 if (!stringJsonFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
988 fprintf(stderr, "Failed to open %s string.json for reading\n", qPrintable(locale));
989 continue;
990 }
991
992 const QByteArray bytes = stringJsonFile.readAll();
993 stringJsonFile.close();
994
995 QJsonParseError parseErr;
996 QJsonDocument doc = QJsonDocument::fromJson(bytes, &parseErr);
997 if (parseErr.error != QJsonParseError::NoError || !doc.isObject()) {
998 fprintf(stderr, "Failed to parse %s string.json: %s\n",
999 qPrintable(locale), qPrintable(parseErr.errorString()));
1000 continue;
1001 }
1002 QJsonObject root = doc.object();
1003 QJsonArray strings = root["string"_L1].toArray();
1004
1005 // Replace QAbility_label value with app name and collect existing names
1006 QSet<QString> existingNames;
1007 for (qsizetype i = 0; i < strings.size(); ++i) {
1008 QJsonObject e = strings[i].toObject();
1009 const QString name = e["name"_L1].toString();
1010 existingNames.insert(name);
1011 if (name == "QAbility_label"_L1) {
1012 e["value"_L1] = displayLabel;
1013 strings[i] = e;
1014 }
1015 }
1016
1017 // Append synthesized permission-reason strings (skip ids already present)
1018 for (const PromotedReason &p : std::as_const(promotedReasons)) {
1019 if (existingNames.contains(p.id))
1020 continue;
1021 QJsonObject e;
1022 e["name"_L1] = p.id;
1023 e["value"_L1] = p.value;
1024 strings.append(e);
1025 existingNames.insert(p.id);
1026 }
1027 root["string"_L1] = strings;
1028 doc.setObject(root);
1029
1030 if (!stringJsonFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
1031 fprintf(stderr, "Failed to open %s string.json for writing\n", qPrintable(locale));
1032 continue;
1033 }
1034
1035 stringJsonFile.write(doc.toJson(QJsonDocument::Indented));
1036 stringJsonFile.close();
1037
1038 if (options.verbose) {
1039 fprintf(stdout,
1040 " Updated %s string.json (label: %s, +%lld promoted permission reasons)\n",
1041 qPrintable(locale), qPrintable(displayLabel),
1042 static_cast<long long>(promotedReasons.size()));
1043 }
1044 }
1045
1046 // Also update AppScope app_name for consistency
1047 QString appScopeStringPath = options.outputDirectory + "/AppScope/resources/base/element/string.json"_L1;
1048 QFile appScopeStringFile(appScopeStringPath);
1049
1050 if (appScopeStringFile.exists()) {
1051 if (!appScopeStringFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
1052 fprintf(stderr, "Failed to open AppScope string.json for reading\n");
1053 return false;
1054 }
1055
1056 QString content = QString::fromUtf8(appScopeStringFile.readAll());
1057 appScopeStringFile.close();
1058
1059 // Replace app_name value
1060 QRegularExpression appNameRegex("(\"name\":\\s*\"app_name\"[^}]*\"value\":\\s*)\"[^\"]*\""_L1);
1061 content.replace(appNameRegex, "\\1\""_L1 + jsonStringEscape(displayLabel) + "\""_L1);
1062
1063 if (!appScopeStringFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
1064 fprintf(stderr, "Failed to open AppScope string.json for writing\n");
1065 return false;
1066 }
1067
1068 appScopeStringFile.write(content.toUtf8());
1069 appScopeStringFile.close();
1070
1071 if (options.verbose)
1072 fprintf(stdout, " Updated AppScope string.json with app name: %s\n",
1073 qPrintable(displayLabel));
1074 }
1075
1076 // Customize module.json5
1077 // Note: We only update the description, not the module name which must remain "entry"
1078 QString moduleJsonPath = options.outputDirectory + "/entry/src/main/module.json5"_L1;
1079 QFile moduleJsonFile(moduleJsonPath);
1080
1081 if (moduleJsonFile.exists()) {
1082 if (!moduleJsonFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
1083 fprintf(stderr, "Failed to open module.json5 for reading\n");
1084 return false;
1085 }
1086
1087 QString content = QString::fromUtf8(moduleJsonFile.readAll());
1088 moduleJsonFile.close();
1089
1090 // Substitute the module description sentinel. The user's value (if set
1091 // via QT_HARMONYOS_MODULE_DESCRIPTION) takes precedence;
1092 // otherwise fall back to the template's $string:module_desc reference,
1093 // which resolves via the entry/.../resources/.../string.json file.
1094 const QString descriptionSentinel = "%%INSERT_MODULE_DESCRIPTION%%"_L1;
1095 const QString descriptionValue = options.harmonyOsModuleDescription.isEmpty()
1096 ? "$string:module_desc"_L1
1097 : jsonStringEscape(options.harmonyOsModuleDescription);
1098 content.replace(descriptionSentinel, descriptionValue);
1099
1100 // Substitute the deviceTypes sentinel.
1101 const QString deviceTypesSentinel = "/* %%INSERT_DEVICE_TYPES%% */"_L1;
1102 QStringList deviceTypes = options.harmonyOsModuleDeviceTypes;
1103 if (deviceTypes.isEmpty())
1104 deviceTypes = QStringList{ "phone"_L1, "tablet"_L1, "2in1"_L1 };
1105 QStringList quotedDeviceTypes;
1106 quotedDeviceTypes.reserve(deviceTypes.size());
1107 for (const QString &dt : std::as_const(deviceTypes))
1108 quotedDeviceTypes.append("\""_L1 + dt + "\""_L1);
1109 content.replace(deviceTypesSentinel, quotedDeviceTypes.join(", "_L1));
1110
1111 // Substitute the ability-orientation sentinel. The template ships the
1112 // sentinel as a block comment so module.json5 stays valid JSON5 when
1113 // the user has not set an orientation; in that case the sentinel line
1114 // is dropped entirely. When set, replace it with the orientation field
1115 // (matching the surrounding 8-space indentation already in the
1116 // template). Unknown values are rejected with a warning rather than
1117 // forwarded to hvigor, which would fail with a less targeted error.
1118 const QString orientationSentinelLine =
1119 " /* %%INSERT_ABILITY_ORIENTATION%% */\n"_L1;
1120 QString orientationReplacement;
1121 if (!options.harmonyOsAbilityOrientation.isEmpty()) {
1122 if (isValidHarmonyOsAbilityOrientation(options.harmonyOsAbilityOrientation)) {
1123 orientationReplacement = " \"orientation\": \""_L1
1124 + options.harmonyOsAbilityOrientation
1125 + "\",\n"_L1;
1126 } else {
1127 fprintf(stderr,
1128 "Warning: Ignoring unknown harmonyos-ability-orientation value '%s'\n",
1129 qPrintable(options.harmonyOsAbilityOrientation));
1130 }
1131 }
1132 content.replace(orientationSentinelLine, orientationReplacement);
1133
1134 // Override the ability/launcher icon so QT_HARMONYOS_APP_ICON
1135 // is reflected on the device home screen, not just in Settings. The
1136 // template ships "$media:layered_image" -- only replace that specific
1137 // value so user-customized icons in subsequent runs aren't clobbered.
1138 if (!iconValue.isEmpty()) {
1139 content.replace(
1140 QRegularExpression("\"icon\":\\s*\"\\$media:layered_image\""_L1),
1141 "\"icon\": \""_L1 + iconValue + "\""_L1);
1142 }
1143
1144 // Build the requestPermissions array fragment from the (possibly
1145 // promoted) transformedPermissions computed above. The sentinel
1146 // "/* %%INSERT_PERMISSIONS%% */" sits inside an empty [] so the
1147 // template stays valid JSON5 even without substitution.
1148 const QString sentinel = "/* %%INSERT_PERMISSIONS%% */"_L1;
1149 QString permissionsFragment;
1150 if (!transformedPermissions.isEmpty()) {
1151 QStringList entryStrings;
1152 entryStrings.reserve(transformedPermissions.size());
1153 for (const QJsonValue &value : std::as_const(transformedPermissions)) {
1154 if (!value.isObject())
1155 continue;
1156 const QJsonObject entry = value.toObject();
1157
1158 // Pretty-print the entry, then re-indent so it lines up with the
1159 // surrounding "requestPermissions" array (8-space base indent).
1160 const QByteArray pretty =
1161 QJsonDocument(entry).toJson(QJsonDocument::Indented).trimmed();
1162 const QStringList lines = QString::fromUtf8(pretty).split(QLatin1Char('\n'));
1163 QStringList indented;
1164 indented.reserve(lines.size());
1165 for (const QString &line : lines)
1166 indented.append(" "_L1 + line);
1167 entryStrings.append(indented.join(QLatin1Char('\n')));
1168 }
1169 if (!entryStrings.isEmpty()) {
1170 permissionsFragment = "\n"_L1 + entryStrings.join(",\n"_L1) + "\n "_L1;
1171 }
1172 }
1173 content.replace(sentinel, permissionsFragment);
1174
1175 if (!moduleJsonFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
1176 fprintf(stderr, "Failed to open module.json5 for writing\n");
1177 return false;
1178 }
1179
1180 moduleJsonFile.write(content.toUtf8());
1181 moduleJsonFile.close();
1182
1183 if (options.verbose) {
1184 fprintf(stdout, " Updated module.json5 description\n");
1185 fprintf(stdout, " Injected %lld permissions into module.json5\n",
1186 static_cast<long long>(transformedPermissions.size()));
1187 }
1188 }
1189
1190 // Customize build-profile.json5 with the SDK version metadata. Only fields
1191 // that are set are substituted; others keep the template default.
1192 //
1193 // * compatibleSdkVersion is an existing key with a default value -- we
1194 // replace its value via the same regex pattern used in app.json5.
1195 // * targetSdkVersion / compileSdkVersion don't appear in the template by
1196 // default. They are added via comment-style sentinels that the JSON5
1197 // parser ignores when not substituted.
1198 {
1199 const bool anySdkVersionSet =
1200 !options.harmonyOsCompatibleSdkVersion.isEmpty()
1201 || !options.harmonyOsTargetSdkVersion.isEmpty()
1202 || !options.harmonyOsCompileSdkVersion.isEmpty();
1203
1204 const QString buildProfilePath =
1205 options.outputDirectory + "/build-profile.json5"_L1;
1206 QFile buildProfileFile(buildProfilePath);
1207 if (anySdkVersionSet && buildProfileFile.exists()) {
1208 if (!buildProfileFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
1209 fprintf(stderr, "Failed to open build-profile.json5 for reading\n");
1210 return false;
1211 }
1212 QString content = QString::fromUtf8(buildProfileFile.readAll());
1213 buildProfileFile.close();
1214
1215 if (!options.harmonyOsCompatibleSdkVersion.isEmpty()) {
1216 content.replace(
1217 QRegularExpression(
1218 "\"compatibleSdkVersion\":\\s*\"[^\"]*\""_L1),
1219 "\"compatibleSdkVersion\": \""_L1
1220 + jsonStringEscape(options.harmonyOsCompatibleSdkVersion)
1221 + "\""_L1);
1222 }
1223
1224 const QString targetSentinel = "/* %%INSERT_TARGET_SDK_VERSION%% */"_L1;
1225 const QString targetReplacement =
1226 options.harmonyOsTargetSdkVersion.isEmpty()
1227 ? QString()
1228 : "\"targetSdkVersion\": \""_L1
1229 + jsonStringEscape(options.harmonyOsTargetSdkVersion)
1230 + "\","_L1;
1231 content.replace(targetSentinel, targetReplacement);
1232
1233 const QString compileSentinel = "/* %%INSERT_COMPILE_SDK_VERSION%% */"_L1;
1234 const QString compileReplacement =
1235 options.harmonyOsCompileSdkVersion.isEmpty()
1236 ? QString()
1237 : "\"compileSdkVersion\": \""_L1
1238 + jsonStringEscape(options.harmonyOsCompileSdkVersion)
1239 + "\","_L1;
1240 content.replace(compileSentinel, compileReplacement);
1241
1242 if (!buildProfileFile.open(
1243 QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
1244 fprintf(stderr, "Failed to open build-profile.json5 for writing\n");
1245 return false;
1246 }
1247 buildProfileFile.write(content.toUtf8());
1248 buildProfileFile.close();
1249
1250 if (options.verbose) {
1251 fprintf(stdout,
1252 " Updated build-profile.json5 SDK versions"
1253 " (compatible=%s, target=%s, compile=%s)\n",
1254 qPrintable(options.harmonyOsCompatibleSdkVersion),
1255 qPrintable(options.harmonyOsTargetSdkVersion),
1256 qPrintable(options.harmonyOsCompileSdkVersion));
1257 }
1258 }
1259 }
1260
1261 if (options.verbose)
1262 fprintf(stdout, "Template customization completed\n");
1263
1264 return true;
1265}
1266
1267static bool copyApplicationBinary(const Options &options)
1268{
1269 if (options.verbose)
1270 fprintf(stdout, "Copying application binary and dependencies\n");
1271
1272 // For each target architecture, copy the application binary
1273 for (const QString &arch : options.targetArchs) {
1274 QString archLibPath = options.outputDirectory + "/entry/libs/"_L1 + arch;
1275 QDir archDir(archLibPath);
1276 if (!archDir.exists()) {
1277 if (!archDir.mkpath("."_L1)) {
1278 fprintf(stderr, "Failed to create architecture directory: %s\n", qPrintable(archLibPath));
1279 return false;
1280 }
1281 }
1282
1283 // Copy application binary
1284 QFileInfo appInfo(options.applicationBinary);
1285 QString destPath = archLibPath + "/"_L1 + appInfo.fileName();
1286
1287 if (!appInfo.fileName().startsWith("lib"_L1)) {
1288 // Ensure it has lib prefix
1289 destPath = archLibPath + "/lib"_L1 + appInfo.fileName();
1290 }
1291
1292 if (!destPath.endsWith(".so"_L1)) {
1293 // Ensure it has .so extension
1294 destPath += ".so"_L1;
1295 }
1296
1297 if (options.verbose) {
1298 fprintf(stdout, " Copying application binary for %s: %s -> %s\n",
1299 qPrintable(arch), qPrintable(options.applicationBinary), qPrintable(destPath));
1300 }
1301
1302 if (!copyFileIfNewer(options.applicationBinary, destPath, options.verbose)) {
1303 fprintf(stderr, "Failed to copy application binary to: %s\n",
1304 qPrintable(destPath));
1305 return false;
1306 }
1307
1308 // Track as dependency for depfile
1309 if (!options.depFilePath.isEmpty())
1310 dependenciesForDepfile << options.applicationBinary;
1311 }
1312
1313 if (options.verbose)
1314 fprintf(stdout, "Application binary copied successfully\n");
1315
1316 return true;
1317}
1318
1319static bool copyFileToArchitectures(const Options &options,
1320 const QString &sourcePath,
1321 const QString &relativeDestPath,
1322 bool trackInDepfile = true)
1323{
1324 for (const QString &arch : options.targetArchs) {
1325 QString destPath = "%1/entry/libs/%2/%3"_L1
1326 .arg(options.outputDirectory, arch, relativeDestPath);
1327
1328 QDir().mkpath(QFileInfo(destPath).absolutePath());
1329
1330 if (options.verbose)
1331 fprintf(stdout, " Copying for %s: %s\n",
1332 qPrintable(arch), qPrintable(QFileInfo(sourcePath).fileName()));
1333
1334 if (!copyFileIfNewer(sourcePath, destPath, options.verbose)) {
1335 fprintf(stderr, "Failed to copy file: %s to %s\n",
1336 qPrintable(sourcePath), qPrintable(destPath));
1337 return false;
1338 }
1339
1340 // Track as dependency for depfile (only once, not per-arch)
1341 if (trackInDepfile && !options.depFilePath.isEmpty() && arch == options.targetArchs.first())
1342 dependenciesForDepfile << sourcePath;
1343 }
1344 return true;
1345}
1346
1347static QString findStdCppLibrary(const Options &options, const QString &arch)
1348{
1349 // Map architecture to NDK triple
1350 QString ndkArch;
1351 if (arch == "arm64-v8a"_L1) {
1352 ndkArch = "aarch64-linux-ohos"_L1;
1353 } else if (arch == "armeabi-v7a"_L1) {
1354 ndkArch = "arm-linux-ohos"_L1;
1355 } else if (arch == "x86_64"_L1) {
1356 ndkArch = "x86_64-linux-ohos"_L1;
1357 } else if (arch == "x86"_L1) {
1358 ndkArch = "i686-linux-ohos"_L1;
1359 } else {
1360 return QString();
1361 }
1362
1363 QString stdCppPath = options.ndkRoot + "/llvm/lib/"_L1 + ndkArch + "/c++/libc++_shared.so"_L1;
1364 if (QFile::exists(stdCppPath))
1365 return stdCppPath;
1366
1367 stdCppPath = options.ndkRoot + "/llvm/lib/"_L1 + ndkArch + "/libc++_shared.so"_L1;
1368 if (QFile::exists(stdCppPath))
1369 return stdCppPath;
1370
1371 return QString();
1372}
1373
1374// Copy project-specific shared libraries (test helper libs, etc.) flat into
1375// entry/libs/<arch>/. These are non-Qt libs listed in "project-libraries" in
1376// the deployment settings JSON, collected from the target's LINK_LIBRARIES by
1377// Qt6HarmonyOSMacros.cmake.
1378static bool copyProjectLibraries(const Options &options)
1379{
1380 if (options.projectLibraries.isEmpty())
1381 return true;
1382
1383 if (options.verbose)
1384 fprintf(stdout, "Copying project libraries\n");
1385
1386 for (const QString &projectLib : options.projectLibraries) {
1387 QFileInfo libInfo(projectLib);
1388 if (!libInfo.exists()) {
1389 if (options.verbose)
1390 fprintf(stdout, " Project library not found, skipping: %s\n",
1391 qPrintable(projectLib));
1392 continue;
1393 }
1394
1395 for (const QString &arch : options.targetArchs) {
1396 QString archLibPath = options.outputDirectory + "/entry/libs/"_L1 + arch;
1397 QString destPath = archLibPath + "/"_L1 + libInfo.fileName();
1398
1399 if (options.verbose) {
1400 fprintf(stdout, " Copying project library for %s: %s\n",
1401 qPrintable(arch), qPrintable(libInfo.fileName()));
1402 }
1403
1404 if (!copyFileIfNewer(projectLib, destPath, options.verbose)) {
1405 fprintf(stderr, "Failed to copy project library to: %s\n",
1406 qPrintable(destPath));
1407 return false;
1408 }
1409
1410 if (!options.depFilePath.isEmpty())
1411 dependenciesForDepfile << projectLib;
1412 }
1413 }
1414
1415 if (options.verbose)
1416 fprintf(stdout, "Project libraries copied successfully\n");
1417
1418 return true;
1419}
1420
1421// Recursively scan dirPath for libtst_*.so test binaries and their co-located helper libs.
1422// Test binaries are appended to found; helper libs (lib*.so* in the same directory as a
1423// test binary) are appended to foundHelpers. helperNames guards against filename collisions
1424// across directories. excludeDirs is a list of absolute paths to skip during recursion.
1425static void scanTestBinariesDir(const QString &dirPath,
1426 const QStringList &excludeList,
1427 const QStringList &excludeDirs,
1428 QStringList &found,
1429 QStringList &foundHelpers,
1430 QSet<QString> &helperNames)
1431{
1432 const QFileInfoList entries =
1433 QDir(dirPath).entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
1434
1435 bool hasTestBinary = false;
1436 for (const QFileInfo &entry : entries) {
1437 if (!entry.isDir()
1438 && entry.fileName().startsWith("libtst_"_L1)
1439 && entry.suffix() == "so"_L1
1440 && !excludeList.contains(entry.fileName())) {
1441 found.append(entry.filePath());
1442 hasTestBinary = true;
1443 }
1444 }
1445
1446 // For each directory that contains a test binary, also collect co-located helper libs
1447 // (e.g. libqmetatype_lib1.so.0) so that $ORIGIN rpath lookups resolve on-device.
1448 if (hasTestBinary) {
1449 for (const QFileInfo &entry : entries) {
1450 if (!entry.isDir()
1451 && entry.fileName().startsWith("lib"_L1)
1452 && !entry.fileName().startsWith("libtst_"_L1)
1453 && entry.fileName().contains(".so"_L1)
1454 && !helperNames.contains(entry.fileName())) {
1455 foundHelpers.append(entry.filePath());
1456 helperNames.insert(entry.fileName());
1457 }
1458 }
1459 }
1460
1461 for (const QFileInfo &entry : entries) {
1462 if (entry.isDir() && !excludeDirs.contains(entry.absoluteFilePath()))
1463 scanTestBinariesDir(entry.filePath(), excludeList, excludeDirs, found, foundHelpers, helperNames);
1464 }
1465}
1466
1467static bool copyTestBinaries(const Options &options, QStringList &bundledBinaries)
1468{
1469 if (options.testBinariesDirectory.isEmpty()) {
1470 fprintf(stderr, "Error: 'test-binaries-directory' not specified for test bundle mode\n");
1471 return false;
1472 }
1473
1474 if (!QDir(options.testBinariesDirectory).exists()) {
1475 fprintf(stderr, "Error: test-binaries-directory does not exist: %s\n",
1476 qPrintable(options.testBinariesDirectory));
1477 return false;
1478 }
1479
1480 if (options.verbose)
1481 fprintf(stdout, "Scanning for test binaries in %s\n", qPrintable(options.testBinariesDirectory));
1482
1483 QStringList found;
1484 QStringList foundHelpers;
1485 QSet<QString> helperNames;
1486 // Exclude the output directory to avoid scanning previously generated HAP bundle contents,
1487 // which would cause libentry.so (built by hvigor/CMake) to be picked up as a helper lib
1488 // and then conflict with the CMake-built version during the hvigor build.
1489 const QStringList excludeDirs = { QFileInfo(options.outputDirectory).absoluteFilePath() };
1490 scanTestBinariesDir(options.testBinariesDirectory, options.testExcludeList, excludeDirs,
1491 found, foundHelpers, helperNames);
1492
1493 if (found.isEmpty()) {
1494 fprintf(stderr, "Warning: No test binaries (libtst_*.so) found in %s\n",
1495 qPrintable(options.testBinariesDirectory));
1496 return true; // Not fatal
1497 }
1498
1499 if (options.verbose) {
1500 fprintf(stdout, "Found %lld test binaries\n", static_cast<long long>(found.size()));
1501 if (!foundHelpers.isEmpty())
1502 fprintf(stdout, "Found %lld test helper libraries\n",
1503 static_cast<long long>(foundHelpers.size()));
1504 }
1505
1506 // Copy all test binaries AND helper libs flat to entry/libs/${arch}/
1507 for (const QString &arch : options.targetArchs) {
1508 QString archLibPath = options.outputDirectory + "/entry/libs/"_L1 + arch;
1509 QDir().mkpath(archLibPath);
1510
1511 for (const QString &testBinary : found) {
1512 QFileInfo testInfo(testBinary);
1513 QString destPath = archLibPath + "/"_L1 + testInfo.fileName();
1514
1515 if (options.verbose)
1516 fprintf(stdout, " Copying test binary: %s\n", qPrintable(testInfo.fileName()));
1517
1518 if (!copyFileIfNewer(testBinary, destPath, options.verbose)) {
1519 fprintf(stderr, "Failed to copy test binary: %s\n", qPrintable(testBinary));
1520 return false;
1521 }
1522 }
1523
1524 for (const QString &helperLib : foundHelpers) {
1525 QFileInfo helperInfo(helperLib);
1526 QString destPath = archLibPath + "/"_L1 + helperInfo.fileName();
1527
1528 if (options.verbose)
1529 fprintf(stdout, " Copying test helper lib: %s\n", qPrintable(helperInfo.fileName()));
1530
1531 if (!copyFileIfNewer(helperLib, destPath, options.verbose)) {
1532 fprintf(stderr, "Failed to copy test helper lib: %s\n", qPrintable(helperLib));
1533 return false;
1534 }
1535 }
1536 }
1537
1538 // Build list of bundled binary filenames (no duplicates)
1539 for (const QString &testBinary : found) {
1540 QString fileName = QFileInfo(testBinary).fileName();
1541 if (!bundledBinaries.contains(fileName))
1542 bundledBinaries.append(fileName);
1543 }
1544
1545 // Track for depfile
1546 if (!options.depFilePath.isEmpty()) {
1547 for (const QString &testBinary : found)
1548 dependenciesForDepfile << testBinary;
1549 for (const QString &helperLib : foundHelpers)
1550 dependenciesForDepfile << helperLib;
1551 }
1552
1553 return true;
1554}
1555
1556static bool writeTestBinariesList(const Options &options, const QStringList &bundledBinaries)
1557{
1558 QString binariesListPath = options.outputDirectory + "/binaries.txt"_L1;
1559
1560 if (options.verbose)
1561 fprintf(stdout, "Writing test binaries list: %s\n", qPrintable(binariesListPath));
1562
1563 QFile file(binariesListPath);
1564 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
1565 fprintf(stderr, "Failed to open binaries.txt for writing: %s\n",
1566 qPrintable(binariesListPath));
1567 return false;
1568 }
1569
1570 for (const QString &binary : bundledBinaries) {
1571 file.write(binary.toUtf8());
1572 file.write("\n");
1573 }
1574 file.close();
1575
1576 if (options.verbose) {
1577 fprintf(stdout, "Wrote %lld test binary names to binaries.txt\n",
1578 static_cast<long long>(bundledBinaries.size()));
1579 }
1580
1581 return true;
1582}
1583
1584static QString readElfSoname(const Options &options, const QString &binaryPath);
1585
1586static bool copyAllQtLibs(const Options &options)
1587{
1588 if (options.qtLibsDirectory.isEmpty())
1589 return true;
1590
1591 if (!QDir(options.qtLibsDirectory).exists()) {
1592 if (options.verbose) {
1593 fprintf(stdout, "Qt libs directory not found, skipping: %s\n",
1594 qPrintable(options.qtLibsDirectory));
1595 }
1596 return true;
1597 }
1598
1599 if (options.verbose)
1600 fprintf(stdout, "Copying all Qt libraries from %s\n", qPrintable(options.qtLibsDirectory));
1601
1602 QDir libsDir(options.qtLibsDirectory);
1603 const QFileInfoList entries = libsDir.entryInfoList({"*.so"_L1, "*.so.*"_L1}, QDir::Files);
1604
1605 for (const QFileInfo &entry : entries) {
1606 for (const QString &arch : options.targetArchs) {
1607 QString destPath = options.outputDirectory + "/entry/libs/"_L1 + arch + "/"_L1 + entry.fileName();
1608 QDir().mkpath(QFileInfo(destPath).absolutePath());
1609 if (!copyFileIfNewer(entry.filePath(), destPath, options.verbose))
1610 return false;
1611 }
1612 if (!options.depFilePath.isEmpty())
1613 dependenciesForDepfile << entry.filePath();
1614 }
1615
1616 // Also copy libc++_shared.so from NDK for each architecture
1617 for (const QString &arch : options.targetArchs) {
1618 QString stdCppPath = findStdCppLibrary(options, arch);
1619 if (!stdCppPath.isEmpty()) {
1620 QString destPath = options.outputDirectory + "/entry/libs/"_L1 + arch + "/libc++_shared.so"_L1;
1621 if (options.verbose)
1622 fprintf(stdout, " Copying C++ standard library for %s\n", qPrintable(arch));
1623 if (!copyFileIfNewer(stdCppPath, destPath, options.verbose))
1624 return false;
1625 if (!options.depFilePath.isEmpty())
1626 dependenciesForDepfile << stdCppPath;
1627 }
1628 }
1629
1630 // Copy all libraries from extra-libs-dirs (e.g. third-party deps like ICU, fontconfig)
1631 for (const QString &extraDir : options.extraLibsDirs) {
1632 QDir dir(extraDir);
1633 if (!dir.exists()) {
1634 if (options.verbose)
1635 fprintf(stdout, "Extra libs dir not found, skipping: %s\n", qPrintable(extraDir));
1636 continue;
1637 }
1638
1639 if (options.verbose)
1640 fprintf(stdout, "Copying extra libraries from %s\n", qPrintable(extraDir));
1641
1642 const QFileInfoList entries = dir.entryInfoList({"*.so"_L1, "*.so.*"_L1}, QDir::Files);
1643 for (const QFileInfo &entry : entries) {
1644 // Deploy using the library's SONAME if it differs from the on-disk filename,
1645 // so the dynamic linker can find it at runtime (e.g. libicudata.so.78,
1646 // not libicudata.so).
1647 const QString soname = readElfSoname(options, entry.filePath());
1648 const QString deployName = (!soname.isEmpty() && soname != entry.fileName())
1649 ? soname : entry.fileName();
1650 for (const QString &arch : options.targetArchs) {
1651 QString destPath = options.outputDirectory + "/entry/libs/"_L1 + arch + "/"_L1 + deployName;
1652 QDir().mkpath(QFileInfo(destPath).absolutePath());
1653 if (!copyFileIfNewer(entry.filePath(), destPath, options.verbose))
1654 return false;
1655 }
1656 if (!options.depFilePath.isEmpty())
1657 dependenciesForDepfile << entry.filePath();
1658 }
1659 }
1660
1661 return true;
1662}
1663
1664static bool copyAllQtPlugins(const Options &options)
1665{
1666 // Copy all plugins from one plugins root directory.
1667 auto copyPluginsFromDir = [&options](const QString &pluginsRootPath) -> bool {
1668 QDir pluginsDir(pluginsRootPath);
1669 const QStringList categories = pluginsDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
1670
1671 for (const QString &category : categories) {
1672 QDir categoryDir(pluginsDir.filePath(category));
1673 const QFileInfoList plugins = categoryDir.entryInfoList({"*.so"_L1}, QDir::Files);
1674
1675 for (const QFileInfo &pluginInfo : plugins) {
1676 const QString &plugin = pluginInfo.fileName();
1677 const QString &pluginPath = pluginInfo.filePath();
1678
1679 if (category == "platforms"_L1 && plugin == "libqohos.so"_L1) {
1680 // Platform plugin goes flat to root libs directory
1681 for (const QString &arch : options.targetArchs) {
1682 QString destPath = options.outputDirectory + "/entry/libs/"_L1 + arch + "/libqohos.so"_L1;
1683 QDir().mkpath(QFileInfo(destPath).absolutePath());
1684 if (!copyFileIfNewer(pluginPath, destPath, options.verbose))
1685 return false;
1686 }
1687 } else {
1688 // All other plugins go into their category subdirectory
1689 QString relativeDestPath = category + "/"_L1 + plugin;
1690 if (!copyFileToArchitectures(options, pluginPath, relativeDestPath, false))
1691 return false;
1692 }
1693
1694 if (!options.depFilePath.isEmpty())
1695 dependenciesForDepfile << pluginPath;
1696 }
1697 }
1698 return true;
1699 };
1700
1701 // Process plugins-import-paths first (typically CMAKE_BINARY_DIR/plugins,
1702 // i.e. the module's own build-tree plugin output). Files written here
1703 // receive dest mtime = now, so the subsequent qtPluginsDirectory pass
1704 // skips any file that was already copied — this gives build-dir contents
1705 // unconditional priority over the installed Qt prefix without requiring a
1706 // force-overwrite flag.
1707 for (const QString &importPath : options.pluginsImportPaths) {
1708 if (!QDir(importPath).exists()) {
1709 if (options.verbose)
1710 fprintf(stdout, "Plugins import path not found, skipping: %s\n",
1711 qPrintable(importPath));
1712 continue;
1713 }
1714 if (options.verbose)
1715 fprintf(stdout, "Copying Qt plugins from import path: %s\n",
1716 qPrintable(importPath));
1717 if (!copyPluginsFromDir(importPath))
1718 return false;
1719 }
1720
1721 // Process qtPluginsDirectory second (the installed Qt prefix). Files
1722 // already present in dest (copied from plugins-import-paths above) are
1723 // skipped by copyFileIfNewer; plugins that exist only in the installed
1724 // prefix are copied normally.
1725 if (options.qtPluginsDirectory.isEmpty())
1726 return true;
1727
1728 if (!QDir(options.qtPluginsDirectory).exists()) {
1729 if (options.verbose) {
1730 fprintf(stdout, "Qt plugins directory not found, skipping: %s\n",
1731 qPrintable(options.qtPluginsDirectory));
1732 }
1733 return true;
1734 }
1735
1736 if (options.verbose)
1737 fprintf(stdout, "Copying all Qt plugins from %s\n", qPrintable(options.qtPluginsDirectory));
1738
1739 return copyPluginsFromDir(options.qtPluginsDirectory);
1740}
1741
1742// Copy user-supplied extra plugins listed in QT_HARMONYOS_EXTRA_PLUGINS into
1743// entry/libs/<arch>/<category>/. The category is derived from the parent
1744// directory of each plugin source path (e.g. .../imageformats/libfoo.so ->
1745// "imageformats/libfoo.so"); plugins without a usable parent directory name
1746// are deployed flat under entry/libs/<arch>/.
1747static bool copyExtraPlugins(const Options &options)
1748{
1749 if (options.extraPlugins.isEmpty())
1750 return true;
1751
1752 if (options.verbose)
1753 fprintf(stdout, "Copying extra plugins\n");
1754
1755 for (const QString &pluginPath : options.extraPlugins) {
1756 const QFileInfo pluginInfo(pluginPath);
1757 if (!pluginInfo.exists() || !pluginInfo.isFile()) {
1758 fprintf(stderr, "Extra plugin does not exist: %s\n", qPrintable(pluginPath));
1759 return false;
1760 }
1761
1762 const QString category = pluginInfo.absoluteDir().dirName();
1763 QString relativeDestPath;
1764 if (category.isEmpty() || category == "plugins"_L1)
1765 relativeDestPath = pluginInfo.fileName();
1766 else
1767 relativeDestPath = category + "/"_L1 + pluginInfo.fileName();
1768
1769 if (options.verbose) {
1770 fprintf(stdout, " Extra plugin: %s -> entry/libs/<arch>/%s\n",
1771 qPrintable(pluginInfo.fileName()), qPrintable(relativeDestPath));
1772 }
1773
1774 if (!copyFileToArchitectures(options, pluginInfo.filePath(), relativeDestPath))
1775 return false;
1776 }
1777
1778 return true;
1779}
1780
1781// .so files in the QML directory go flat to entry/libs/<arch>/; all other files
1782// preserve directory structure under resfile/qml/.
1783static bool copyQmlDir(const QString &srcDir, const QString &relPath,
1784 const QString &qmlDestBase, const Options &options)
1785{
1786 const QFileInfoList entries =
1787 QDir(srcDir).entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden);
1788
1789 for (const QFileInfo &entry : entries) {
1790 const QString entryRelPath = relPath.isEmpty()
1791 ? entry.fileName()
1792 : relPath + "/"_L1 + entry.fileName();
1793
1794 if (entry.isDir()) {
1795 if (!copyQmlDir(entry.filePath(), entryRelPath, qmlDestBase, options))
1796 return false;
1797 } else if (entry.suffix() == "so"_L1) {
1798 for (const QString &arch : options.targetArchs) {
1799 QString destPath = options.outputDirectory + "/entry/libs/"_L1
1800 + arch + "/"_L1 + entry.fileName();
1801 QDir().mkpath(QFileInfo(destPath).absolutePath());
1802 if (!copyFileIfNewer(entry.filePath(), destPath, options.verbose))
1803 return false;
1804 }
1805 if (!options.depFilePath.isEmpty())
1806 dependenciesForDepfile << entry.filePath();
1807 } else {
1808 QString destPath = qmlDestBase + "/"_L1 + entryRelPath;
1809 QDir().mkpath(QFileInfo(destPath).absolutePath());
1810 if (!copyFileIfNewer(entry.filePath(), destPath, options.verbose))
1811 return false;
1812 if (!options.depFilePath.isEmpty())
1813 dependenciesForDepfile << entry.filePath();
1814 }
1815 }
1816 return true;
1817}
1818
1819static QString hapQmlDir(const Options &options)
1820{
1821 return options.outputDirectory + "/entry/src/main/resources/resfile/qml"_L1;
1822}
1823
1824static bool copyAllQmlModules(const Options &options)
1825{
1826 const QString qmlDestBase = hapQmlDir(options);
1827
1828 // Process qml-import-paths first (typically CMAKE_BINARY_DIR/qml, i.e. the
1829 // module's own build-tree QML output). Files written here receive dest
1830 // mtime = now, so the subsequent qtQmlDirectory pass skips any file that
1831 // was already copied — this gives build-dir contents unconditional priority
1832 // over the installed Qt prefix without requiring a force-overwrite flag.
1833 for (const QString &importPath : options.qmlImportPaths) {
1834 if (!QDir(importPath).exists()) {
1835 if (options.verbose)
1836 fprintf(stdout, "QML import path not found, skipping: %s\n",
1837 qPrintable(importPath));
1838 continue;
1839 }
1840 if (options.verbose)
1841 fprintf(stdout, "Copying QML modules from import path: %s\n",
1842 qPrintable(importPath));
1843 if (!copyQmlDir(importPath, QString(), qmlDestBase, options))
1844 return false;
1845 }
1846
1847 // Process qtQmlDirectory second (the installed Qt prefix). Files that are
1848 // already present in dest (copied from qml-import-paths above) are skipped
1849 // by copyFileIfNewer; files that exist only here — e.g. QtQml/QtQuick when
1850 // building qtlottie against an installed qtdeclarative — are copied normally.
1851 if (options.qtQmlDirectory.isEmpty())
1852 return true;
1853 if (!QDir(options.qtQmlDirectory).exists()) {
1854 if (options.verbose)
1855 fprintf(stdout, "QML directory not found, skipping: %s\n",
1856 qPrintable(options.qtQmlDirectory));
1857 return true;
1858 }
1859 if (options.verbose)
1860 fprintf(stdout, "Copying all QML modules from %s\n",
1861 qPrintable(options.qtQmlDirectory));
1862 return copyQmlDir(options.qtQmlDirectory, QString(), qmlDestBase, options);
1863}
1864
1865static bool readQmldirLines(const QString &qmldirPath, QStringList &lines)
1866{
1867 QFile qmldir(qmldirPath);
1868 if (!qmldir.open(QIODevice::ReadOnly | QIODevice::Text)) {
1869 fprintf(stderr, "Failed to open qmldir %s: %s\n",
1870 qPrintable(qmldirPath), qPrintable(qmldir.errorString()));
1871 return false;
1872 }
1873
1874 lines = QString::fromUtf8(qmldir.readAll()).split(u'\n');
1875
1876 return true;
1877}
1878
1879// Deploy the plugins (*.so files) a qmldir declares to libs/<arch>.
1880static bool copyQmldirPlugins(const QString &qmldirPath, const Options &options)
1881{
1882 QStringList lines;
1883 if (!readQmldirLines(qmldirPath, lines))
1884 return false;
1885
1886 static const QRegularExpression pluginLine(
1887 "^\\s*(?:optional\\s+)?plugin\\s+(?<name>\\S+)(?:\\s+(?<path>\\S+))?"_L1);
1888
1889 const QDir moduleDir = QFileInfo(qmldirPath).absoluteDir();
1890
1891 for (const QString &line : lines) {
1892 const QRegularExpressionMatch match = pluginLine.match(line);
1893 if (!match.hasMatch())
1894 continue;
1895
1896 const QString pluginName = match.captured(u"name");
1897 const QString pluginFile = "lib"_L1 + pluginName + ".so"_L1;
1898 const QString optionalPluginPath = match.captured(u"path");
1899 const QString pluginSrc = moduleDir.filePath(
1900 optionalPluginPath.isEmpty() ? pluginFile : optionalPluginPath + "/"_L1 + pluginFile);
1901
1902 if (!QFileInfo::exists(pluginSrc)) {
1903 if (options.verbose) {
1904 fprintf(stdout, " qmldir %s declares plugin %s, but %s is missing\n",
1905 qPrintable(qmldirPath), qPrintable(pluginName),
1906 qPrintable(pluginSrc));
1907 }
1908 continue;
1909 }
1910
1911 if (!copyFileToArchitectures(options, pluginSrc, pluginFile))
1912 return false;
1913 }
1914
1915 return true;
1916}
1917
1918static bool getQmldirModuleUri(const QString &qmldirPath, QString &uri)
1919{
1920 QStringList lines;
1921 if (!readQmldirLines(qmldirPath, lines))
1922 return false;
1923
1924 static const QRegularExpression moduleLine("^\\s*module\\s+(?<uri>\\S+)"_L1);
1925 for (const QString &line : lines) {
1926 const QRegularExpressionMatch match = moduleLine.match(line);
1927 if (match.hasMatch()) {
1928 uri = match.captured(u"uri");
1929 return true;
1930 }
1931 }
1932
1933 uri.clear();
1934
1935 return true;
1936}
1937
1938static QString findNearestEnclosingTestDir(const QString &startDir, const QSet<QString> &testDirs)
1939{
1940 for (QDir dir(startDir); ; ) {
1941 const QString path = dir.absolutePath();
1942
1943 if (testDirs.contains(path))
1944 return path;
1945
1946 if (!dir.cdUp())
1947 return QString();
1948 }
1949}
1950
1956
1957static bool getTestQmlModuleDeployDir(const TestQmlModule &testModule, QString &deployDir)
1958{
1959 QString uri;
1960 if (!getQmldirModuleUri(testModule.qmldirPath, uri))
1961 return false;
1962
1963 if (uri.isEmpty()) {
1964 deployDir = QDir(testModule.testDir).relativeFilePath(
1965 QFileInfo(testModule.qmldirPath).absolutePath());
1966 } else {
1967 deployDir = uri.replace(u'.', u'/');
1968 }
1969
1970 return true;
1971}
1972
1974{
1975 if (options.testBinariesDirectory.isEmpty())
1976 return {};
1977
1978 QStringList testBinaries;
1979 QStringList unusedHelperLibs;
1980 QSet<QString> unusedHelperLibNames;
1981 const QStringList excludeDirs = { QFileInfo(options.outputDirectory).absoluteFilePath() };
1982 scanTestBinariesDir(options.testBinariesDirectory, options.testExcludeList, excludeDirs,
1983 testBinaries, unusedHelperLibs, unusedHelperLibNames);
1984
1985 QSet<QString> testDirs;
1986 for (const QString &testBinary : std::as_const(testBinaries))
1987 testDirs.insert(QFileInfo(testBinary).absoluteDir().absolutePath());
1988
1989 QList<TestQmlModule> modules;
1990 QSet<QString> seenQmldirs;
1991 for (const QString &testDir : std::as_const(testDirs)) {
1992 QDirIterator it(testDir, { "qmldir"_L1 }, QDir::Files, QDirIterator::Subdirectories);
1993 while (it.hasNext()) {
1994 const QString qmldirPath = it.next();
1995 if (seenQmldirs.contains(qmldirPath))
1996 continue;
1997
1998 const QString testDir =
1999 findNearestEnclosingTestDir(QFileInfo(qmldirPath).absolutePath(), testDirs);
2000 seenQmldirs.insert(qmldirPath);
2001 modules.append({ qmldirPath, testDir });
2002 }
2003 }
2004
2005 return modules;
2006}
2007
2008// Fail if two test QML modules in the bundle deploy to the same directory under
2009// resfile/qml, where one would overwrite the other.
2010static bool verifyUniqueTestQmlModuleDeployDirs(const QList<TestQmlModule> &modules)
2011{
2012 QHash<QString, QString> deployDirToQmldir;
2013 for (const TestQmlModule &module : modules) {
2014 QString deployDir;
2015 if (!getTestQmlModuleDeployDir(module, deployDir))
2016 return false;
2017
2018 const auto existing = deployDirToQmldir.constFind(deployDir);
2019 if (existing != deployDirToQmldir.constEnd()) {
2020 fprintf(stderr,
2021 "Error: two test QML modules deploy to the same directory \"%s\" "
2022 "under resfile/qml:\n %s\n %s\n"
2023 "They would overwrite one another; give one a different module URI.\n",
2024 qPrintable(deployDir), qPrintable(*existing), qPrintable(module.qmldirPath));
2025 return false;
2026 }
2027 deployDirToQmldir.insert(deployDir, module.qmldirPath);
2028 }
2029
2030 return true;
2031}
2032
2033static bool copyTestQmlModuleFiles(const QString &moduleSrcDirPath,
2034 const QString &destModuleDirPath, const Options &options)
2035{
2036 static const QStringList qmlModuleFileNameFilters = {
2037 "qmldir"_L1,
2038 "*.qmltypes"_L1,
2039 "*.qml"_L1,
2040 "*.js"_L1,
2041 "*.mjs"_L1,
2042 };
2043
2044 const QDir moduleSrcDir(moduleSrcDirPath);
2045 QDirIterator it(
2046 moduleSrcDirPath, qmlModuleFileNameFilters, QDir::Files, QDirIterator::Subdirectories);
2047 while (it.hasNext()) {
2048 const QString srcPath = it.next();
2049 const QString destPath =
2050 destModuleDirPath + "/"_L1 + moduleSrcDir.relativeFilePath(srcPath);
2051
2052 QDir().mkpath(QFileInfo(destPath).absolutePath());
2053
2054 if (!copyFileIfNewer(srcPath, destPath, options.verbose))
2055 return false;
2056
2057 if (!options.depFilePath.isEmpty())
2058 dependenciesForDepfile << srcPath;
2059
2060 if (it.fileName() == "qmldir"_L1 && !copyQmldirPlugins(srcPath, options))
2061 return false;
2062 }
2063
2064 return true;
2065}
2066
2067// Deploy each test's generated QML modules into resfile/qml
2068static bool copyTestQmlModules(const QList<TestQmlModule> &modules, const Options &options)
2069{
2070 for (const TestQmlModule &module : modules) {
2071 const QString moduleSrcDir = QFileInfo(module.qmldirPath).absolutePath();
2072
2073 QString deployDir;
2074 if (!getTestQmlModuleDeployDir(module, deployDir))
2075 return false;
2076
2077 const QString destModuleDir = hapQmlDir(options) + "/"_L1 + deployDir;
2078 if (!copyTestQmlModuleFiles(moduleSrcDir, destModuleDir, options))
2079 return false;
2080 }
2081
2082 return true;
2083}
2084
2085static QString findLlvmReadobj(const Options &options)
2086{
2087 // Look for llvm-readobj in the NDK and alternative path
2088 const QStringList searchPaths = {
2089 options.ndkRoot + "/llvm/bin"_L1,
2090 options.sdkRoot + "/command-line-tools/sdk/default/openharmony/native/llvm/bin"_L1
2091 };
2092
2093 const QString llvmReadobj = QStandardPaths::findExecutable("llvm-readobj"_L1, searchPaths);
2094 if (!llvmReadobj.isEmpty())
2095 return llvmReadobj;
2096
2097 return QString();
2098}
2099
2100struct QtDependency
2101{
2102 QString relativePath; // e.g., "lib/libQt6Core.so"
2103 QString absolutePath; // Full path on filesystem
2104};
2105
2106// Returns the SONAME embedded in the ELF dynamic section of the library, or an empty
2107// string if it cannot be determined. The SONAME may differ from the on-disk filename
2108// (e.g. libicudata.so has SONAME libicudata.so.78).
2109static QString readElfSoname(const Options &options, const QString &binaryPath)
2110{
2111 const QString llvmReadobj = findLlvmReadobj(options);
2112 if (llvmReadobj.isEmpty())
2113 return QString();
2114
2115 QProcess process;
2116 process.start(llvmReadobj, {"--dynamic"_L1, binaryPath});
2117 if (!process.waitForStarted() || !process.waitForFinished(30000))
2118 return QString();
2119
2120 const QString output = QString::fromUtf8(process.readAllStandardOutput());
2121 // Output line: " 0x...E SONAME Library soname: [libfoo.so.1]"
2122 for (const auto &line : output.split('\n'_L1)) {
2123 if (!line.contains("SONAME"_L1))
2124 continue;
2125 const int lb = line.indexOf('['_L1);
2126 const int rb = line.indexOf(']'_L1, lb);
2127 if (lb >= 0 && rb > lb)
2128 return line.mid(lb + 1, rb - lb - 1);
2129 }
2130 return QString();
2131}
2132
2133static QStringList readElfDependencies(const Options &options, const QString &binaryPath)
2134{
2135 QString llvmReadobj = findLlvmReadobj(options);
2136 if (llvmReadobj.isEmpty()) {
2137 fprintf(stderr, "Warning: llvm-readobj not found, cannot detect dependencies\n");
2138 return QStringList();
2139 }
2140
2141 QProcess process;
2142 QStringList arguments;
2143 arguments << "--needed-libs"_L1 << binaryPath;
2144
2145 process.start(llvmReadobj, arguments);
2146 if (!process.waitForStarted()) {
2147 fprintf(stderr, "Failed to start llvm-readobj\n");
2148 return QStringList();
2149 }
2150
2151 if (!process.waitForFinished(30000)) { // 30 second timeout
2152 fprintf(stderr, "llvm-readobj timed out\n");
2153 process.kill();
2154 return QStringList();
2155 }
2156
2157 if (process.exitCode() != 0) {
2158 fprintf(stderr, "llvm-readobj failed with exit code %d\n", process.exitCode());
2159 return QStringList();
2160 }
2161
2162 QStringList dependencies;
2163 QString output = QString::fromUtf8(process.readAllStandardOutput());
2164 QStringList lines = output.split('\n'_L1);
2165
2166 bool inNeededLibs = false;
2167 for (const QString &line : lines) {
2168 QString trimmed = line.trimmed();
2169
2170 if (trimmed.startsWith("NeededLibraries"_L1)) {
2171 inNeededLibs = true;
2172 continue;
2173 }
2174
2175 if (!inNeededLibs)
2176 continue;
2177
2178 // Stop at next section
2179 if (trimmed.isEmpty() || trimmed.contains(':'_L1))
2180 break;
2181
2182 // Extract library name
2183 if (trimmed.startsWith("lib"_L1))
2184 dependencies.append(trimmed);
2185 }
2186
2187 return dependencies;
2188}
2189
2190static QString findExtraDepLibrary(const Options &options, const QString &libName)
2191{
2192 for (const QString &dir : options.extraLibsDirs) {
2193 // Try exact name first (e.g. libicudata.so.78)
2194 QString libPath = dir + "/"_L1 + libName;
2195 if (QFile::exists(libPath))
2196 return libPath;
2197 // Fall back to unversioned name (e.g. libicudata.so) for libraries whose
2198 // SONAME carries a version suffix but the file on disk does not.
2199 int soIdx = libName.indexOf(".so."_L1);
2200 if (soIdx >= 0) {
2201 QString baseName = libName.left(soIdx + 3); // up to and including ".so"
2202 libPath = dir + "/"_L1 + baseName;
2203 if (QFile::exists(libPath))
2204 return libPath;
2205 }
2206 }
2207 return QString();
2208}
2209
2210static bool isSystemLibrary(const QString &libName)
2211{
2212 // System libraries that should not be bundled
2213 return libName.startsWith("libc."_L1) ||
2214 libName.startsWith("libm."_L1) ||
2215 libName.startsWith("libdl."_L1) ||
2216 libName == "libEGL.so"_L1 ||
2217 libName == "libGLESv2.so"_L1 ||
2218 libName == "libGLESv3.so"_L1 ||
2219 libName.startsWith("libz."_L1) ||
2220 libName == "libc++_shared.so"_L1;
2221}
2222
2223static QString findQtLibrary(const Options &options, const QString &libName)
2224{
2225 // Use qtLibsDirectory if provided (preferred method, from JSON config)
2226 if (!options.qtLibsDirectory.isEmpty()) {
2227 QString libPath = options.qtLibsDirectory + "/"_L1 + libName;
2228 if (QFile::exists(libPath))
2229 return libPath;
2230 }
2231
2232 // Fallback: walk up from application binary to find Qt installation
2233 QFileInfo appInfo(options.applicationBinary);
2234 QDir dir(appInfo.absolutePath());
2235 for (int i = 0; i < 10; ++i) {
2236 // Check qtbase/lib (common for non-prefix builds)
2237 QString qtbaseLib = dir.absoluteFilePath("qtbase/lib"_L1);
2238 if (QDir(qtbaseLib).exists()) {
2239 QString libPath = qtbaseLib + "/"_L1 + libName;
2240 if (QFile::exists(libPath))
2241 return libPath;
2242 }
2243 // Check lib directory
2244 QString libDir = dir.absoluteFilePath("lib"_L1);
2245 if (QDir(libDir).exists()) {
2246 QString libPath = libDir + "/"_L1 + libName;
2247 if (QFile::exists(libPath))
2248 return libPath;
2249 }
2250 if (!dir.cdUp())
2251 break;
2252 }
2253
2254 return QString();
2255}
2256
2257static bool detectAndCopyDependencies(const Options &options, QSet<QString> &processedLibs)
2258{
2259 if (options.verbose)
2260 fprintf(stdout, "Detecting Qt library dependencies\n");
2261
2262 // Start with the application binary
2263 QStringList toProcess;
2264 toProcess.append(options.applicationBinary);
2265
2266 // Add project libraries to the ELF dependency analysis queue so their Qt
2267 // dependencies are also transitively scanned and deployed.
2268 for (const QString &projectLib : options.projectLibraries)
2269 toProcess.append(projectLib);
2270
2271 QList<QtDependency> qtDependencies;
2272 QStringList detectedQtModules; // Track detected Qt module names
2273 bool needsStdCpp = false;
2274
2275 while (!toProcess.isEmpty()) {
2276 QString currentLib = toProcess.takeFirst();
2277
2278 if (processedLibs.contains(currentLib))
2279 continue;
2280
2281 processedLibs.insert(currentLib);
2282
2283 if (options.verbose)
2284 fprintf(stdout, " Analyzing: %s\n", qPrintable(QFileInfo(currentLib).fileName()));
2285
2286 QStringList deps = readElfDependencies(options, currentLib);
2287
2288 for (const QString &dep : deps) {
2289 // Check if we need C++ standard library
2290 if (dep == "libc++_shared.so"_L1) {
2291 needsStdCpp = true;
2292 continue;
2293 }
2294
2295 // Skip system libraries
2296 if (isSystemLibrary(dep))
2297 continue;
2298
2299 // Only process Qt libraries, platform plugins, and third-party libs from extra dirs
2300 if (!dep.startsWith("libQt6"_L1) && !dep.startsWith("libqohos"_L1)) {
2301 // Check extra library search directories (e.g. HARMONYOS_DEPS_ROOT/lib)
2302 QString extraDepPath = findExtraDepLibrary(options, dep);
2303 if (extraDepPath.isEmpty())
2304 continue;
2305 // Guard against duplicates without blocking recursive ELF scanning:
2306 // do NOT insert into processedLibs here — the while-loop dequeue does
2307 // that, which also ensures the library's own ELF deps get scanned.
2308 if (!processedLibs.contains(extraDepPath) && !toProcess.contains(extraDepPath)) {
2309 if (options.verbose)
2310 fprintf(stdout, " Found extra dep: %s\n", qPrintable(dep));
2311 QtDependency extraDep;
2312 extraDep.relativePath = "lib/"_L1 + dep;
2313 extraDep.absolutePath = extraDepPath;
2314 qtDependencies.append(extraDep);
2315 toProcess.append(extraDepPath);
2316 }
2317 continue;
2318 }
2319
2320 // Extract module name from Qt library (e.g., libQt6Core.so -> Core)
2321 if (dep.startsWith("libQt6"_L1)) {
2322 QString moduleName = dep.mid(6); // Skip "libQt6"
2323 if (moduleName.endsWith(".so"_L1))
2324 moduleName.chop(3);
2325 if (!moduleName.isEmpty() && !detectedQtModules.contains(moduleName))
2326 detectedQtModules.append(moduleName);
2327 }
2328
2329 QString depPath = findQtLibrary(options, dep);
2330 if (depPath.isEmpty()) {
2331 if (options.verbose)
2332 fprintf(stdout, " Warning: Could not find Qt library: %s\n", qPrintable(dep));
2333 continue;
2334 }
2335
2336 if (processedLibs.contains(depPath))
2337 continue;
2338
2339 if (options.verbose)
2340 fprintf(stdout, " Found dependency: %s\n", qPrintable(dep));
2341
2342 QtDependency qtDep;
2343 qtDep.relativePath = "lib/"_L1 + dep;
2344 qtDep.absolutePath = depPath;
2345 qtDependencies.append(std::move(qtDep));
2346
2347 // Add to processing queue for recursive dependency detection
2348 toProcess.append(std::move(depPath));
2349 }
2350 }
2351
2352 if (options.verbose) {
2353 fprintf(stdout, "Found %lld Qt library dependencies\n", static_cast<long long>(qtDependencies.size()));
2354 if (needsStdCpp)
2355 fprintf(stdout, "C++ standard library required\n");
2356 }
2357
2358 // Copy C++ standard library if needed (per-arch, since source path is arch-specific)
2359 if (needsStdCpp) {
2360 for (const QString &arch : options.targetArchs) {
2361 QString archLibPath = options.outputDirectory + "/entry/libs/"_L1 + arch;
2362 QString stdCppPath = findStdCppLibrary(options, arch);
2363 if (stdCppPath.isEmpty()) {
2364 fprintf(stderr, "Warning: Could not find C++ standard library for %s\n", qPrintable(arch));
2365 } else {
2366 QString destPath = archLibPath + "/libc++_shared.so"_L1;
2367 if (options.verbose)
2368 fprintf(stdout, " Copying C++ standard library for %s\n", qPrintable(arch));
2369 if (!copyFileIfNewer(stdCppPath, destPath, options.verbose)) {
2370 fprintf(stderr,
2371 "Failed to copy C++ standard library to: %s\n",
2372 qPrintable(destPath));
2373 return false;
2374 }
2375
2376 // Track as dependency for depfile
2377 if (!options.depFilePath.isEmpty())
2378 dependenciesForDepfile << stdCppPath;
2379 }
2380 }
2381 }
2382
2383 // Copy all detected Qt/extra libraries to all target architectures.
2384 // copyFileToArchitectures handles all arches internally; call it once per library.
2385 // Use dep.relativePath filename so the deployed name matches the ELF SONAME
2386 // (e.g. libicudata.so.78, not the unversioned on-disk name libicudata.so).
2387 for (const QtDependency &dep : qtDependencies) {
2388 QFileInfo libInfo(dep.relativePath);
2389 if (!copyFileToArchitectures(options, dep.absolutePath, libInfo.fileName(), false))
2390 return false;
2391
2392 if (!options.depFilePath.isEmpty())
2393 dependenciesForDepfile << dep.absolutePath;
2394 }
2395
2396 return true;
2397}
2398
2400{
2401 // 1. Use qtPluginsDirectory if provided (from JSON config)
2402 if (!options.qtPluginsDirectory.isEmpty() &&
2403 QDir(options.qtPluginsDirectory).exists()) {
2404 return options.qtPluginsDirectory;
2405 }
2406
2407 // 2. Fallback: walk up from application binary
2408 QFileInfo appInfo(options.applicationBinary);
2409 QDir dir(appInfo.absolutePath());
2410 for (int i = 0; i < 10; ++i) {
2411 // Check qtbase/plugins for modular builds
2412 QString candidate = dir.absoluteFilePath("qtbase/plugins"_L1);
2413 if (QDir(candidate).exists())
2414 return candidate;
2415
2416 // Check plugins directory
2417 if (dir.exists("plugins"_L1))
2418 return dir.absoluteFilePath("plugins"_L1);
2419
2420 if (!dir.cdUp())
2421 break;
2422 }
2423
2424 return QString();
2425}
2426
2427static bool copyPlatformPlugin(const Options &options,
2428 const QString &qtPluginsPath,
2429 QSet<QString> &processedLibs)
2430{
2431 // Copy libqohos.so to ROOT libs directory (not in platforms subdirectory)
2432 QString qohosPlugin = qtPluginsPath + "/platforms/libqohos.so"_L1;
2433 if (!QFile::exists(qohosPlugin)) {
2434 fprintf(stderr, "Warning: Platform plugin libqohos.so not found at: %s\n",
2435 qPrintable(qohosPlugin));
2436 return true; // Not fatal
2437 }
2438
2439 // Detect dependencies of libqohos.so
2440 if (options.verbose)
2441 fprintf(stdout, " Detecting platform plugin dependencies\n");
2442
2443 QStringList pluginDeps = readElfDependencies(options, qohosPlugin);
2444 QList<QtDependency> additionalLibs;
2445
2446 for (const QString &dep : pluginDeps) {
2447 // Only process Qt libraries we haven't already copied
2448 if (!dep.startsWith("libQt6"_L1))
2449 continue;
2450
2451 QString depPath = findQtLibrary(options, dep);
2452 if (depPath.isEmpty()) {
2453 if (options.verbose)
2454 fprintf(stdout, " Warning: Could not find plugin dependency: %s\n",
2455 qPrintable(dep));
2456 continue;
2457 }
2458
2459 if (processedLibs.contains(depPath))
2460 continue;
2461
2462 if (options.verbose)
2463 fprintf(stdout, " Found plugin dependency: %s\n", qPrintable(dep));
2464
2465 processedLibs.insert(depPath);
2466 QtDependency qtDep;
2467 qtDep.relativePath = "lib/"_L1 + dep;
2468 qtDep.absolutePath = std::move(depPath);
2469 additionalLibs.append(std::move(qtDep));
2470 }
2471
2472 // Copy additional Qt libraries needed by the plugin
2473 for (const QString &arch : options.targetArchs) {
2474 QString archLibPath = options.outputDirectory + "/entry/libs/"_L1 + arch;
2475
2476 for (const QtDependency &dep : additionalLibs) {
2477 QString destPath = archLibPath + "/"_L1 + QFileInfo(dep.absolutePath).fileName();
2478
2479 if (options.verbose) {
2480 fprintf(stdout, " Copying plugin dependency for %s: %s\n",
2481 qPrintable(arch), qPrintable(QFileInfo(dep.absolutePath).fileName()));
2482 }
2483
2484 if (!copyFileIfNewer(dep.absolutePath, destPath, options.verbose)) {
2485 fprintf(stderr, "Failed to copy library: %s\n", qPrintable(destPath));
2486 return false;
2487 }
2488
2489 // Track as dependency for depfile
2490 if (!options.depFilePath.isEmpty())
2491 dependenciesForDepfile << dep.absolutePath;
2492 }
2493 }
2494
2495 // Now copy libqohos.so itself to root
2496 for (const QString &arch : options.targetArchs) {
2497 QString archLibPath = options.outputDirectory + "/entry/libs/"_L1 + arch;
2498 QString destPath = archLibPath + "/libqohos.so"_L1;
2499
2500 if (options.verbose)
2501 fprintf(stdout, " Copying platform plugin for %s: libqohos.so (to root)\n",
2502 qPrintable(arch));
2503
2504 if (!copyFileIfNewer(qohosPlugin, destPath, options.verbose)) {
2505 fprintf(stderr, "Failed to copy libqohos.so to: %s\n", qPrintable(destPath));
2506 return false;
2507 }
2508
2509 // Track as dependency for depfile
2510 if (!options.depFilePath.isEmpty())
2511 dependenciesForDepfile << qohosPlugin;
2512 }
2513
2514 return true;
2515}
2516
2517static bool copyPlugins(const Options &options, QSet<QString> &processedLibs)
2518{
2519 if (options.verbose)
2520 fprintf(stdout, "Copying required Qt plugins\n");
2521
2522 // Find Qt plugins directory
2523 QString qtPluginsPath = findQtPluginsDirectory(options);
2524 if (qtPluginsPath.isEmpty()) {
2525 fprintf(stderr, "Warning: Could not find Qt plugins directory\n");
2526 return true; // Not fatal
2527 }
2528
2529 if (options.verbose)
2530 fprintf(stdout, " Qt plugins directory: %s\n", qPrintable(qtPluginsPath));
2531
2532 // Copy platform plugin (special case: goes to root, not platforms/)
2533 if (!copyPlatformPlugin(options, qtPluginsPath, processedLibs))
2534 return false;
2535
2536 // Discover and copy all other plugins based on dependencies
2537 QDir pluginsDir(qtPluginsPath);
2538 QStringList pluginCategories = pluginsDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
2539
2540 if (options.verbose)
2541 fprintf(stdout, "Scanning plugin categories: %s\n",
2542 qPrintable(pluginCategories.join(", "_L1)));
2543
2544 for (const QString &category : pluginCategories) {
2545 // Skip platforms category - libqohos.so already handled specially above
2546 if (category == "platforms"_L1)
2547 continue;
2548
2549 QDir categoryDir(pluginsDir.filePath(category));
2550 QStringList plugins = categoryDir.entryList({"*.so"_L1}, QDir::Files);
2551
2552 if (plugins.isEmpty())
2553 continue;
2554
2555 if (options.verbose)
2556 fprintf(stdout, "\nChecking %s plugins (%lld found):\n",
2557 qPrintable(category), static_cast<long long>(plugins.size()));
2558
2559 for (const QString &plugin : plugins) {
2560 QString pluginPath = categoryDir.filePath(plugin);
2561
2562 // Read plugin's ELF dependencies
2563 QStringList deps = readElfDependencies(options, pluginPath);
2564
2565 // Check if all Qt dependencies are satisfied
2566 bool allDepsSatisfied = true;
2567 QStringList unsatisfiedDeps;
2568
2569 for (const QString &dep : deps) {
2570 // Skip system libraries
2571 if (isSystemLibrary(dep))
2572 continue;
2573
2574 // Check if Qt6 dependency is being included
2575 if (dep.startsWith("libQt6"_L1)) {
2576 QString depPath = findQtLibrary(options, dep);
2577 if (depPath.isEmpty() || !processedLibs.contains(depPath)) {
2578 allDepsSatisfied = false;
2579 unsatisfiedDeps.append(dep);
2580 }
2581 }
2582 }
2583
2584 if (allDepsSatisfied) {
2585 // Copy this plugin to entry/libs/{arch}/{category}/
2586 if (options.verbose)
2587 fprintf(stdout, " [✓] %s (dependencies satisfied)\n", qPrintable(plugin));
2588
2589 QString relativeDestPath = "%1/%2"_L1.arg(category, plugin);
2590 if (!copyFileToArchitectures(options, pluginPath, relativeDestPath))
2591 return false;
2592 } else {
2593 if (options.verbose) {
2594 fprintf(stdout, " [✗] %s (missing: %s)\n",
2595 qPrintable(plugin), qPrintable(unsatisfiedDeps.join(", "_L1)));
2596 }
2597 }
2598 }
2599 }
2600
2601 if (options.verbose)
2602 fprintf(stdout, "Plugin copying completed\n");
2603
2604 return true;
2605}
2606
2608{
2609 QString name; // Module name (e.g., "QtQuick")
2610 QString path; // Absolute path to module directory
2611 QString type; // "module" or "plugin"
2612 QString plugin; // Plugin name (e.g., "qtquick2plugin")
2613 bool pluginIsOptional = false; // Whether plugin is optional
2614 QString prefer; // Preferred location (e.g., ":/" means embedded in resources)
2615 QStringList components; // List of QML component file paths
2616 QStringList scripts; // List of JavaScript file paths
2617};
2618
2619static QList<QmlImportInfo> scanQmlImports(const Options &options, bool *ok = nullptr)
2620{
2621 if (ok)
2622 *ok = true;
2623
2624 QList<QmlImportInfo> imports;
2625
2626 if (options.qmlRootPaths.isEmpty()) {
2627 if (options.verbose)
2628 fprintf(stdout,
2629 "No QML root path specified, skipping QML import scanning\n");
2630 return imports;
2631 }
2632
2633 if (options.verbose)
2634 fprintf(stdout, "Scanning for QML imports\n");
2635
2636 // 1. Use qtLibExecsDirectory if provided (preferred, from JSON config)
2637 QStringList searchPaths;
2638 if (!options.qtLibExecsDirectory.isEmpty())
2639 searchPaths.append(options.qtLibExecsDirectory);
2640
2641 // 2. Try qtHostDirectory/bin as fallback
2642 if (!options.qtHostDirectory.isEmpty())
2643 searchPaths.append(options.qtHostDirectory + "/bin"_L1);
2644
2645 QString qmlImportScannerPath =
2646 QStandardPaths::findExecutable("qmlimportscanner"_L1, searchPaths);
2647
2648 // 3. Fallback: search from application binary path
2649 if (qmlImportScannerPath.isEmpty()) {
2650 QDir dir(QFileInfo(options.applicationBinary).absolutePath());
2651
2652 for (int i = 0; i < 10; ++i) {
2653 qmlImportScannerPath = QStandardPaths::findExecutable(
2654 "qmlimportscanner"_L1,
2655 {dir.absoluteFilePath("libexec"_L1), dir.absoluteFilePath("bin"_L1)});
2656
2657 if (!qmlImportScannerPath.isEmpty())
2658 break;
2659
2660 if (!dir.cdUp())
2661 break;
2662 }
2663 }
2664
2665 if (qmlImportScannerPath.isEmpty()) {
2666 fprintf(
2667 stderr,
2668 "Warning: qmlimportscanner not found, skipping QML import scanning\n");
2669 return imports;
2670 }
2671
2672 if (options.verbose)
2673 fprintf(stdout, " Using qmlimportscanner: %s\n",
2674 qPrintable(qmlImportScannerPath));
2675
2676 // Build import paths argument
2677 QStringList importPaths;
2678
2679 // Add QML import paths from config
2680 for (const QString &path : options.qmlImportPaths)
2681 if (QFile::exists(path))
2682 importPaths.append(path);
2683
2684 // Add application build directory for locally-built QML modules
2685 // This is needed to find modules like "shared" that are built alongside the
2686 // app
2687 QFileInfo appBinary(options.applicationBinary);
2688 QString appBuildDir = appBinary.absolutePath();
2689 if (QDir(appBuildDir).exists())
2690 importPaths.append(appBuildDir);
2691
2692 // Add Qt QML directory (target platform)
2693 if (!options.qtQmlDirectory.isEmpty() &&
2694 QDir(options.qtQmlDirectory).exists()) {
2695 importPaths.append(options.qtQmlDirectory);
2696 } else {
2697 // Fallback: search from application binary path
2698 QFileInfo appBinary(options.applicationBinary);
2699 QDir qtDir(appBinary.absolutePath());
2700 for (int i = 0; i < 10; ++i) {
2701 QString qmlDir = qtDir.absoluteFilePath("qml"_L1);
2702 if (QDir(qmlDir).exists()) {
2703 importPaths.append(qmlDir);
2704 break;
2705 }
2706 // Also check qtbase/../qml for modular builds
2707 qmlDir = qtDir.absoluteFilePath("qtbase/../qml"_L1);
2708 if (QDir(qmlDir).exists()) {
2709 importPaths.append(QDir::cleanPath(qmlDir));
2710 break;
2711 }
2712 if (!qtDir.cdUp())
2713 break;
2714 }
2715 }
2716
2717 if (importPaths.isEmpty()) {
2718 fprintf(stderr, "Warning: No QML import paths found\n");
2719 return imports;
2720 }
2721
2722 // Build qmlimportscanner command. qmlimportscanner accepts a -rootPath flag
2723 // per root, so emit them in order.
2724 QStringList arguments;
2725 for (const QString &rootPath : options.qmlRootPaths)
2726 arguments << "-rootPath"_L1 << rootPath;
2727
2728 for (const QString &importPath : importPaths)
2729 arguments << "-importPath"_L1 << importPath;
2730
2731 if (options.verbose) {
2732 fprintf(stdout, " Root paths:\n");
2733 for (const QString &rootPath : options.qmlRootPaths)
2734 fprintf(stdout, " %s\n", qPrintable(rootPath));
2735 fprintf(stdout, " Import paths:\n");
2736 for (const QString &path : importPaths)
2737 fprintf(stdout, " %s\n", qPrintable(path));
2738 }
2739
2740 // Run qmlimportscanner
2741 QProcess process;
2742 process.start(qmlImportScannerPath, arguments);
2743
2744 if (!process.waitForFinished(30000)) {
2745 process.kill();
2746 process.waitForFinished(2000);
2747 fprintf(stderr, "Error: qmlimportscanner timed out after 30 seconds\n");
2748 fprintf(stderr, " Command: %s %s\n", qPrintable(qmlImportScannerPath),
2749 qPrintable(arguments.join(u' ')));
2750 const QByteArray errorOutput = process.readAllStandardError();
2751 if (!errorOutput.isEmpty())
2752 fprintf(stderr, "%s\n", errorOutput.constData());
2753 if (ok)
2754 *ok = false;
2755 return imports;
2756 }
2757
2758 if (process.exitStatus() != QProcess::NormalExit) {
2759 fprintf(stderr, "Error: qmlimportscanner crashed\n");
2760 fprintf(stderr, "%s\n", process.readAllStandardError().constData());
2761 if (ok)
2762 *ok = false;
2763 return imports;
2764 }
2765
2766 if (process.exitCode() != 0) {
2767 fprintf(stderr, "Error: qmlimportscanner failed with exit code %d\n",
2768 process.exitCode());
2769 fprintf(stderr, "%s\n", process.readAllStandardError().constData());
2770 if (ok)
2771 *ok = false;
2772 return imports;
2773 }
2774
2775 // Parse JSON output
2776 QByteArray output = process.readAllStandardOutput();
2777 QJsonDocument doc = QJsonDocument::fromJson(output);
2778
2779 if (!doc.isArray()) {
2780 fprintf(stderr, "Error: Invalid JSON output from qmlimportscanner\n");
2781 if (ok)
2782 *ok = false;
2783 return imports;
2784 }
2785
2786 QJsonArray array = doc.array();
2787 for (const QJsonValue &value : array) {
2788 if (!value.isObject())
2789 continue;
2790
2791 QJsonObject obj = value.toObject();
2792 QmlImportInfo info;
2793 info.name = obj["name"_L1].toString();
2794 info.path = obj["path"_L1].toString();
2795 info.type = obj["type"_L1].toString();
2796
2797 if (obj.contains("plugin"_L1))
2798 info.plugin = obj["plugin"_L1].toString();
2799
2800 if (obj.contains("pluginIsOptional"_L1))
2801 info.pluginIsOptional = obj["pluginIsOptional"_L1].toBool();
2802
2803 if (obj.contains("prefer"_L1))
2804 info.prefer = obj["prefer"_L1].toString();
2805
2806 // Parse components array
2807 if (obj.contains("components"_L1)) {
2808 QJsonArray componentsArray = obj["components"_L1].toArray();
2809 for (const QJsonValue &comp : componentsArray)
2810 info.components.append(comp.toString());
2811 }
2812
2813 // Parse scripts array
2814 if (obj.contains("scripts"_L1)) {
2815 QJsonArray scriptsArray = obj["scripts"_L1].toArray();
2816 for (const QJsonValue &script : scriptsArray)
2817 info.scripts.append(script.toString());
2818 }
2819
2820 // Skip if path is empty (unresolved import)
2821 if (info.path.isEmpty()) {
2822 if (options.verbose)
2823 fprintf(stdout, " Warning: Could not resolve QML import: %s\n",
2824 qPrintable(info.name));
2825 continue;
2826 }
2827
2828 // Skip if type is not module
2829 if (info.type != "module"_L1)
2830 continue;
2831
2832 if (options.verbose)
2833 fprintf(stdout, " Found QML import: %s at %s\n", qPrintable(info.name),
2834 qPrintable(info.path));
2835
2836 imports.append(info);
2837 }
2838
2839 return imports;
2840}
2841
2842static bool copyQmlFiles(const Options &options)
2843{
2844 if (options.qmlRootPaths.isEmpty())
2845 return true; // Not an error, just no QML files to copy
2846
2847 // Target directory: entry/src/main/resources/rawfile/qml/.
2848 //
2849 // Multiple roots are merged into the same destination. copyFileIfNewer
2850 // only overwrites when the source mtime is newer than the destination,
2851 // so in practice the *first* root to land a given file wins. User-set
2852 // QT_QML_ROOT_PATH values are emitted before auto-collected roots (see
2853 // _qt_internal_harmonyos_format_deployment_paths in Qt6HarmonyOSMacros.cmake),
2854 // giving explicit user settings precedence over auto-collected ones on
2855 // collision.
2856 const QString destDir =
2857 options.outputDirectory + "/entry/src/main/resources/rawfile/qml"_L1;
2858 if (!QDir().mkpath(destDir)) {
2859 fprintf(stderr, "Failed to create QML destination directory: %s\n",
2860 qPrintable(destDir));
2861 return false;
2862 }
2863
2864 for (const QString &rootPath : options.qmlRootPaths) {
2865 if (options.verbose)
2866 fprintf(stdout, "Copying QML files from %s\n", qPrintable(rootPath));
2867
2868 if (!copyRecursively(rootPath, destDir, options.verbose)) {
2869 fprintf(stderr, "Failed to copy QML files from %s\n", qPrintable(rootPath));
2870 return false;
2871 }
2872 }
2873
2874 if (options.verbose)
2875 fprintf(stdout, "QML files copied successfully\n");
2876
2877 return true;
2878}
2879
2880static bool copyQmlImports(const Options &options,
2881 const QList<QmlImportInfo> &imports,
2882 QSet<QString> &processedLibs)
2883{
2884 if (imports.isEmpty())
2885 return true;
2886
2887 if (options.verbose)
2888 fprintf(stdout, "Copying QML imports\n");
2889
2890 // QML non-.so files go to resfile/qml/ (maintaining directory structure)
2891 // QML plugin .so files go to libs/arm64-v8a/ (flat, no subdirectory)
2892 QString qmlDestBase = hapQmlDir(options);
2893 QDir().mkpath(qmlDestBase);
2894
2895 // Track QML plugins to scan for dependencies
2896 QStringList qmlPluginsToScan;
2897
2898 for (const QmlImportInfo &import : imports) {
2899 if (options.verbose)
2900 fprintf(stdout, " Copying QML module: %s\n", qPrintable(import.name));
2901
2902 // Determine module subdirectory - preserve full path structure
2903 // e.g., QtQuick.Window should go to QtQuick/Window/, not just Window/
2904 QString relativePath;
2905
2906 // Calculate relative path from Qt QML directory
2907 if (!options.qtQmlDirectory.isEmpty() && import.path.startsWith(options.qtQmlDirectory)) {
2908 // Qt module - use relative path from Qt QML dir
2909 relativePath = import.path.mid(options.qtQmlDirectory.length());
2910 if (relativePath.startsWith('/'_L1))
2911 relativePath = relativePath.mid(1);
2912 } else {
2913 // Application module or other - use module name converted to path
2914 // e.g., "QtQuick.Window" -> "QtQuick/Window"
2915 relativePath = import.name;
2916 relativePath.replace('.'_L1, '/'_L1);
2917 }
2918
2919 QString destModuleDir = qmlDestBase + "/"_L1 + relativePath;
2920 QDir().mkpath(destModuleDir);
2921
2922 // Copy qmldir file (required for module discovery)
2923 QString qmldirSrc = import.path + "/qmldir"_L1;
2924 QString qmldirDest = destModuleDir + "/qmldir"_L1;
2925 if (QFile::exists(qmldirSrc)) {
2926 if (copyFileIfNewer(qmldirSrc, qmldirDest, options.verbose)) {
2927 if (options.verbose)
2928 fprintf(stdout, " Copied qmldir\n");
2929
2930 // Track as dependency for depfile
2931 if (!options.depFilePath.isEmpty())
2932 dependenciesForDepfile << qmldirSrc;
2933 } else {
2934 fprintf(stderr, "Warning: Failed to copy qmldir for %s\n",
2935 qPrintable(import.name));
2936 }
2937 }
2938
2939 // Copy plugin library if not embedded in resources
2940 // Check "prefer" field - if it starts with ":/" then QML files are in
2941 // resources
2942 bool qmlFilesAreEmbedded = import.prefer.startsWith(":/"_L1);
2943
2944 if (!import.plugin.isEmpty()) {
2945 // QML plugin .so files go to libs/arm64-v8a/ (flat, per HarmonyOS requirements)
2946 QString pluginFileName = "lib"_L1 + import.plugin + ".so"_L1;
2947 QString pluginSrc = import.path + "/"_L1 + pluginFileName;
2948
2949 if (QFile::exists(pluginSrc)) {
2950 // Copy to libs directory for each architecture
2951 for (const QString &arch : options.targetArchs) {
2952 QString pluginDest = options.outputDirectory + "/entry/libs/"_L1 + arch
2953 + "/"_L1 + pluginFileName;
2954
2955 if (copyFileIfNewer(pluginSrc, pluginDest, options.verbose)) {
2956 if (options.verbose)
2957 fprintf(stdout, " Copied plugin to libs/%s: %s\n",
2958 qPrintable(arch), qPrintable(pluginFileName));
2959 processedLibs.insert(pluginSrc);
2960
2961 // Add plugin to list for dependency scanning
2962 if (!qmlPluginsToScan.contains(pluginSrc))
2963 qmlPluginsToScan.append(pluginSrc);
2964
2965 // Track as dependency for depfile
2966 if (!options.depFilePath.isEmpty())
2967 dependenciesForDepfile << pluginSrc;
2968 } else if (!import.pluginIsOptional) {
2969 fprintf(stderr, "Warning: Failed to copy required plugin: %s\n",
2970 qPrintable(pluginFileName));
2971 }
2972 }
2973 } else if (!import.pluginIsOptional) {
2974 if (options.verbose)
2975 fprintf(stdout, " Warning: Required plugin not found: %s\n",
2976 qPrintable(pluginFileName));
2977 }
2978 }
2979
2980 // Copy QML component files (only if not embedded in resources)
2981 if (!qmlFilesAreEmbedded) {
2982 for (const QString &component : import.components) {
2983 QFileInfo compInfo(component);
2984 if (!compInfo.exists()) {
2985 if (options.verbose)
2986 fprintf(stdout, " Warning: Component file not found: %s\n",
2987 qPrintable(component));
2988 continue;
2989 }
2990
2991 QString relativePath = component.mid(import.path.length());
2992 if (relativePath.startsWith('/'_L1))
2993 relativePath = relativePath.mid(1);
2994
2995 QString destFile = destModuleDir + "/"_L1 + relativePath;
2996 QFileInfo destInfo(destFile);
2997 QDir().mkpath(destInfo.absolutePath());
2998
2999 if (copyFileIfNewer(component, destFile, options.verbose)) {
3000 if (options.verbose)
3001 fprintf(stdout, " Copied component: %s\n", qPrintable(compInfo.fileName()));
3002
3003 // Track as dependency for depfile
3004 if (!options.depFilePath.isEmpty())
3005 dependenciesForDepfile << component;
3006 }
3007 }
3008
3009 // Copy JavaScript files
3010 for (const QString &script : import.scripts) {
3011 QFileInfo scriptInfo(script);
3012 if (!scriptInfo.exists())
3013 continue;
3014
3015 QString relativePath = script.mid(import.path.length());
3016 if (relativePath.startsWith('/'_L1))
3017 relativePath = relativePath.mid(1);
3018
3019 QString destFile = destModuleDir + "/"_L1 + relativePath;
3020 QFileInfo destInfo(destFile);
3021 QDir().mkpath(destInfo.absolutePath());
3022
3023 if (copyFileIfNewer(script, destFile, options.verbose)) {
3024 if (options.verbose)
3025 fprintf(stdout, " Copied script: %s\n", qPrintable(scriptInfo.fileName()));
3026
3027 // Track as dependency for depfile
3028 if (!options.depFilePath.isEmpty())
3029 dependenciesForDepfile << script;
3030 }
3031 }
3032 } else {
3033 if (options.verbose)
3034 fprintf(stdout, " Skipping QML files (embedded in resources)\n");
3035 }
3036 }
3037
3038 // Scan QML plugin dependencies and copy any missing Qt libraries
3039 if (!qmlPluginsToScan.isEmpty()) {
3040 if (options.verbose)
3041 fprintf(stdout, "Scanning QML plugin dependencies\n");
3042
3043 QStringList toProcess = qmlPluginsToScan;
3044 while (!toProcess.isEmpty()) {
3045 QString pluginPath = toProcess.takeFirst();
3046
3047 if (options.verbose)
3048 fprintf(stdout, " Scanning plugin: %s\n", qPrintable(QFileInfo(pluginPath).fileName()));
3049
3050 QStringList deps = readElfDependencies(options, pluginPath);
3051 for (const QString &dep : deps) {
3052 // Skip system libraries
3053 if (isSystemLibrary(dep))
3054 continue;
3055
3056 // Resolve the library path from Qt libs or extra-libs-dirs
3057 QString depPath;
3058 if (dep.startsWith("libQt6"_L1)) {
3059 depPath = findQtLibrary(options, dep);
3060 if (depPath.isEmpty()) {
3061 if (options.verbose)
3062 fprintf(stdout, " Warning: Could not find Qt library: %s\n", qPrintable(dep));
3063 continue;
3064 }
3065 } else if (!options.extraLibsDirs.isEmpty()) {
3066 depPath = findExtraDepLibrary(options, dep);
3067 if (depPath.isEmpty())
3068 continue;
3069 } else {
3070 continue;
3071 }
3072
3073 if (processedLibs.contains(depPath))
3074 continue;
3075
3076 if (options.verbose)
3077 fprintf(stdout, " Found dependency: %s\n", qPrintable(dep));
3078
3079 // Copy the library
3080 for (const QString &arch : options.targetArchs) {
3081 QString destPath = options.outputDirectory + "/entry/libs/"_L1 + arch + "/"_L1 + dep;
3082 if (copyFileIfNewer(depPath, destPath, options.verbose)) {
3083 if (options.verbose)
3084 fprintf(stdout, " Copied %s to libs/%s\n", qPrintable(dep), qPrintable(arch));
3085
3086 // Track as dependency for depfile
3087 if (!options.depFilePath.isEmpty())
3088 dependenciesForDepfile << depPath;
3089 }
3090 }
3091
3092 processedLibs.insert(depPath);
3093 // Recursively scan this dependency
3094 toProcess.append(depPath);
3095 }
3096 }
3097 }
3098
3099 if (options.verbose)
3100 fprintf(stdout, "QML imports copied successfully\n");
3101
3102 return true;
3103}
3104
3105// Signing material resolved from the --signing-* flags and the matching
3106// QT_HARMONYOS_SIGNING_* environment variables.
3108{
3109 bool enabled = false;
3110 // hvigor-encrypted passwords are left to hvigor's sign task, the only one that
3111 // can decrypt them.
3120};
3121
3122// Signing is all-or-nothing: setting any input commits to signing, so a missing
3123// one is an error rather than a silently unsigned HAP.
3124static bool resolveSigningConfig(const Options &options, SigningConfig *signing)
3125{
3126 struct Field
3127 {
3128 const char *envName;
3129 const char *cliFlag;
3130 QString cliValue;
3131 QString envValue;
3132
3133 QString resolved() const
3134 {
3135 if (!cliValue.isEmpty())
3136 return cliValue;
3137 return envValue;
3138 }
3139 QString label() const
3140 {
3141 return QString::fromLatin1(cliFlag) + " / "_L1 + QString::fromLatin1(envName);
3142 }
3143 };
3144
3145 Field required[] = {
3146 { "QT_HARMONYOS_SIGNING_CERT_PATH", "--signing-cert-path", options.signingCertPath,
3147 qEnvironmentVariable("QT_HARMONYOS_SIGNING_CERT_PATH") },
3148 { "QT_HARMONYOS_SIGNING_PROFILE", "--signing-profile", options.signingProfile,
3149 qEnvironmentVariable("QT_HARMONYOS_SIGNING_PROFILE") },
3150 { "QT_HARMONYOS_SIGNING_STORE_FILE", "--signing-store-file", options.signingStoreFile,
3151 qEnvironmentVariable("QT_HARMONYOS_SIGNING_STORE_FILE") },
3152 { "QT_HARMONYOS_SIGNING_KEY_ALIAS", "--signing-key-alias", options.signingKeyAlias,
3153 qEnvironmentVariable("QT_HARMONYOS_SIGNING_KEY_ALIAS") },
3154 };
3155
3156 // Each password is required in exactly one of its two forms: encrypted, plain.
3157 Field passwords[][2] = {
3158 { { "QT_HARMONYOS_SIGNING_KEY_PASSWORD", "--signing-key-password",
3159 options.signingKeyPassword, qEnvironmentVariable("QT_HARMONYOS_SIGNING_KEY_PASSWORD") },
3160 { "QT_HARMONYOS_SIGNING_PLAIN_KEY_PASSWORD", "--signing-plain-key-password",
3161 options.signingPlainKeyPassword,
3162 qEnvironmentVariable("QT_HARMONYOS_SIGNING_PLAIN_KEY_PASSWORD") } },
3163 { { "QT_HARMONYOS_SIGNING_STORE_PASSWORD", "--signing-store-password",
3164 options.signingStorePassword,
3165 qEnvironmentVariable("QT_HARMONYOS_SIGNING_STORE_PASSWORD") },
3166 { "QT_HARMONYOS_SIGNING_PLAIN_STORE_PASSWORD", "--signing-plain-store-password",
3167 options.signingPlainStorePassword,
3168 qEnvironmentVariable("QT_HARMONYOS_SIGNING_PLAIN_STORE_PASSWORD") } },
3169 };
3170
3171 QString signAlg = options.signingAlg.isEmpty()
3172 ? qEnvironmentVariable("QT_HARMONYOS_SIGNING_ALG")
3173 : options.signingAlg;
3174
3175 bool anySet = !signAlg.isEmpty();
3176 for (const Field &f : required)
3177 anySet = anySet || !f.resolved().isEmpty();
3178 for (const auto &forms : passwords)
3179 anySet = anySet || !forms[0].resolved().isEmpty() || !forms[1].resolved().isEmpty();
3180 if (!anySet)
3181 return true;
3182
3183 QStringList missing;
3184 for (const Field &f : required) {
3185 if (f.resolved().isEmpty())
3186 missing << f.label();
3187 }
3188 for (const auto &forms : passwords) {
3189 if (forms[0].resolved().isEmpty() && forms[1].resolved().isEmpty())
3190 missing << forms[0].label() + " or "_L1 + forms[1].label();
3191 }
3192 if (!missing.isEmpty()) {
3193 fprintf(stderr, "Error: HAP signing requested, but the following required input(s)\n"
3194 " are missing (neither CLI flag nor env var was supplied):\n");
3195 for (const QString &name : missing)
3196 fprintf(stderr, " %s\n", qPrintable(name));
3197 return false;
3198 }
3199
3200 for (const auto &forms : passwords) {
3201 if (!forms[0].resolved().isEmpty() && !forms[1].resolved().isEmpty()) {
3202 fprintf(stderr, "Error: both %s and %s are set; pass only one of them\n",
3203 qPrintable(forms[0].label()), qPrintable(forms[1].label()));
3204 return false;
3205 }
3206 }
3207
3208 const bool encryptedKeyPassword = !passwords[0][0].resolved().isEmpty();
3209 const bool encryptedStorePassword = !passwords[1][0].resolved().isEmpty();
3210 if (encryptedKeyPassword != encryptedStorePassword) {
3211 fprintf(stderr,
3212 "Error: the key password and the keystore password must both be\n"
3213 " hvigor-encrypted or both plain text.\n");
3214 return false;
3215 }
3216
3217 signing->enabled = true;
3218 signing->encryptedPasswords = encryptedKeyPassword;
3219 signing->certPath = required[0].resolved();
3220 signing->profile = required[1].resolved();
3221 signing->storeFile = required[2].resolved();
3222 signing->keyAlias = required[3].resolved();
3223 signing->keyPassword = passwords[0][encryptedKeyPassword ? 0 : 1].resolved();
3224 signing->storePassword = passwords[1][encryptedStorePassword ? 0 : 1].resolved();
3225 signing->signAlg = signAlg.isEmpty() ? "SHA256withECDSA"_L1 : signAlg;
3226
3227 const std::pair<const char *, QString> files[] = {
3228 { required[0].cliFlag, signing->certPath },
3229 { required[1].cliFlag, signing->profile },
3230 { required[2].cliFlag, signing->storeFile },
3231 };
3232 for (const auto &file : files) {
3233 if (!QFile::exists(file.second)) {
3234 fprintf(stderr, "Error: signing input %s does not exist: %s\n", file.first,
3235 qPrintable(file.second));
3236 return false;
3237 }
3238 }
3239
3240 if (options.verbose) {
3241 fprintf(stdout, "HAP signing enabled\n");
3242 fprintf(stdout, " certpath: %s\n", qPrintable(signing->certPath));
3243 fprintf(stdout, " profile: %s\n", qPrintable(signing->profile));
3244 fprintf(stdout, " storeFile: %s\n", qPrintable(signing->storeFile));
3245 fprintf(stdout, " keyAlias: %s\n", qPrintable(signing->keyAlias));
3246 fprintf(stdout, " signAlg: %s\n", qPrintable(signing->signAlg));
3247 fprintf(stdout, " passwords: %s\n",
3248 signing->encryptedPasswords ? "hvigor-encrypted" : "plain text");
3249 }
3250 return true;
3251}
3252
3253// build-profile.json5 ships with `"signingConfigs": []`; replace that literal
3254// with a populated block, so that hvigor signs the HAP with encrypted passwords.
3255// JSON5 input (trailing commas, // comments) precludes QJsonDocument, hence
3256// string surgery.
3257static bool injectSigningConfig(const Options &options, const SigningConfig &signing)
3258{
3259 if (!signing.enabled || !signing.encryptedPasswords)
3260 return true;
3261
3262 const QString buildProfilePath = options.outputDirectory + "/build-profile.json5"_L1;
3263 QFile profileFile(buildProfilePath);
3264 if (!profileFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
3265 fprintf(stderr, "Error: cannot open %s for reading: %s\n",
3266 qPrintable(buildProfilePath), qPrintable(profileFile.errorString()));
3267 return false;
3268 }
3269 QByteArray content = profileFile.readAll();
3270 profileFile.close();
3271
3272 // Anything other than the literal empty array means the user already edited
3273 // the file; refuse rather than risk corrupting it.
3274 static const QByteArray needle = "\"signingConfigs\": []";
3275 const int idx = content.indexOf(needle);
3276 if (idx < 0) {
3277 fprintf(stderr, "Error: '%s' not found in %s. The template may have been modified;\n"
3278 " cannot inject signing configuration safely.\n",
3279 needle.constData(), qPrintable(buildProfilePath));
3280 return false;
3281 }
3282
3283 // Only backslash and double-quote need escaping; inputs are paths, aliases,
3284 // and hex blobs — no control characters.
3285 auto escapeJsonString = [](const QByteArray &in) {
3286 QByteArray out;
3287 out.reserve(in.size());
3288 for (char c : in) {
3289 if (c == '\\' || c == '"')
3290 out.append('\\');
3291 out.append(c);
3292 }
3293 return out;
3294 };
3295
3296 QByteArray replacement;
3297 replacement.append("\"signingConfigs\": [\n");
3298 replacement.append(" {\n");
3299 replacement.append(" \"name\": \"default\",\n");
3300 replacement.append(" \"type\": \"HarmonyOS\",\n");
3301 replacement.append(" \"material\": {\n");
3302 auto appendField = [&](const char *key, const QString &value, bool last) {
3303 replacement.append(" \"");
3304 replacement.append(key);
3305 replacement.append("\": \"");
3306 replacement.append(escapeJsonString(value.toUtf8()));
3307 replacement.append('"');
3308 if (!last)
3309 replacement.append(',');
3310 replacement.append('\n');
3311 };
3312 appendField("certpath", signing.certPath, false);
3313 appendField("keyAlias", signing.keyAlias, false);
3314 appendField("keyPassword", signing.keyPassword, false);
3315 appendField("profile", signing.profile, false);
3316 appendField("signAlg", signing.signAlg, false);
3317 appendField("storeFile", signing.storeFile, false);
3318 appendField("storePassword", signing.storePassword, true);
3319 replacement.append(" }\n");
3320 replacement.append(" }\n");
3321 replacement.append(" ]");
3322
3323 content.replace(idx, needle.size(), replacement);
3324
3325 if (!profileFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
3326 fprintf(stderr, "Error: cannot open %s for writing: %s\n",
3327 qPrintable(buildProfilePath), qPrintable(profileFile.errorString()));
3328 return false;
3329 }
3330 if (profileFile.write(content) != content.size()) {
3331 fprintf(stderr, "Error: failed to write full content to %s\n",
3332 qPrintable(buildProfilePath));
3333 profileFile.close();
3334 return false;
3335 }
3336 profileFile.close();
3337
3338 if (options.verbose)
3339 fprintf(stdout, "Injected HAP signingConfig into %s\n", qPrintable(buildProfilePath));
3340 return true;
3341}
3342
3344{
3345 const QString envJava = qEnvironmentVariable("QT_HARMONYOS_JAVA");
3346 if (!envJava.isEmpty())
3347 return envJava;
3348
3349 const QString javaHome = qEnvironmentVariable("JAVA_HOME");
3350 if (!javaHome.isEmpty()) {
3351 const QString fromHome =
3352 QStandardPaths::findExecutable("java"_L1, { javaHome + "/bin"_L1 });
3353 if (!fromHome.isEmpty())
3354 return fromHome;
3355 }
3356
3357 return QStandardPaths::findExecutable("java"_L1);
3358}
3359
3360static QString findHapSignTool(const Options &options)
3361{
3362 const QString envTool = qEnvironmentVariable("QT_HARMONYOS_HAP_SIGN_TOOL");
3363 if (!envTool.isEmpty())
3364 return envTool;
3365
3366 if (options.sdkRoot.isEmpty())
3367 return QString();
3368
3369 // OHOS_SDK_ROOT normally points at the openharmony directory; accept the
3370 // enclosing directories too, matching the other SDK lookups in this tool.
3371 const QStringList candidates = {
3372 options.sdkRoot + "/toolchains/lib/hap-sign-tool.jar"_L1,
3373 options.sdkRoot + "/openharmony/toolchains/lib/hap-sign-tool.jar"_L1,
3374 options.sdkRoot
3375 + "/command-line-tools/sdk/default/openharmony/toolchains/lib/hap-sign-tool.jar"_L1,
3376 };
3377 for (const QString &candidate : candidates) {
3378 if (QFile::exists(candidate))
3379 return candidate;
3380 }
3381 return QString();
3382}
3383
3384// hap-sign-tool falls back to API level 9 when -compatibleVersion is omitted,
3385// which understates what the manifest declares. Recover the real level from the
3386// generated build-profile.json5, where it is written as "6.1.0(23)".
3388{
3389 QFile profileFile(options.outputDirectory + "/build-profile.json5"_L1);
3390 if (!profileFile.open(QIODevice::ReadOnly | QIODevice::Text))
3391 return QString();
3392 const QString content = QString::fromUtf8(profileFile.readAll());
3393
3394 static const QRegularExpression keyRe("\"compatibleSdkVersion\"\\s*:\\s*([^,\\n}]+)"_L1);
3395 const QRegularExpressionMatch keyMatch = keyRe.match(content);
3396 if (!keyMatch.hasMatch())
3397 return QString();
3398
3399 QString value = keyMatch.captured(1).trimmed();
3400 static const QRegularExpression apiRe("\\‍((\\d+)\\‍)"_L1);
3401 const QRegularExpressionMatch apiMatch = apiRe.match(value);
3402 if (apiMatch.hasMatch())
3403 return apiMatch.captured(1);
3404
3405 value.remove(u'"');
3406 value = value.trimmed();
3407 bool ok = false;
3408 value.toInt(&ok);
3409 return ok ? value : QString();
3410}
3411
3412// Sign the HAP that hvigor produced, using the SDK's hap-sign-tool. With
3413// encrypted passwords hvigor has signed it already, and only its path changes.
3414//
3415// hvigor's own sign task runs both passwords through a decryptor keyed on the
3416// contents of DevEco Studio's material directory, so it rejects plain passwords.
3417// hap-sign-tool takes the passwords as given, which is the form material from
3418// AGC, a CI secret store or Qt Creator's keychain comes in. See QTBUG-149206.
3419static bool signHap(const Options &options, const SigningConfig &signing, QString *hapPath)
3420{
3421 if (!signing.enabled)
3422 return true;
3423 if (hapPath->isEmpty()) {
3424 fprintf(stderr, "Warning: HAP signing is configured, but no HAP was built to sign\n");
3425 return true;
3426 }
3427
3428 const QFileInfo unsignedInfo(*hapPath);
3429 QString baseName = unsignedInfo.completeBaseName();
3430 if (baseName.endsWith("-unsigned"_L1))
3431 baseName.chop(9);
3432 const QString signedPath = unsignedInfo.absolutePath() + "/"_L1 + baseName + "-signed.hap"_L1;
3433
3434 if (signing.encryptedPasswords) {
3435 if (!QFile::exists(signedPath)) {
3436 fprintf(stderr, "Error: hvigor did not produce the signed HAP %s\n",
3437 qPrintable(signedPath));
3438 return false;
3439 }
3440 *hapPath = signedPath;
3441 return true;
3442 }
3443
3444 const QString signTool = findHapSignTool(options);
3445 if (signTool.isEmpty()) {
3446 fprintf(stderr,
3447 "Error: hap-sign-tool.jar not found. Set QT_HARMONYOS_HAP_SIGN_TOOL\n"
3448 " to its path, or configure with OHOS_SDK_ROOT set to the SDK\n"
3449 " that contains it.\n");
3450 return false;
3451 }
3452
3453 const QString javaExecutable = findJavaExecutable();
3454 if (javaExecutable.isEmpty()) {
3455 fprintf(stderr,
3456 "Error: no java executable found for running hap-sign-tool.\n"
3457 " Set QT_HARMONYOS_JAVA or JAVA_HOME, or add java to PATH.\n");
3458 return false;
3459 }
3460
3461 // hap-sign-tool refuses to overwrite, and a leftover from an earlier build
3462 // would otherwise be mistaken for this build's output.
3463 if (QFile::exists(signedPath) && !QFile::remove(signedPath)) {
3464 fprintf(stderr, "Error: cannot remove stale %s\n", qPrintable(signedPath));
3465 return false;
3466 }
3467
3468 QStringList arguments;
3469 arguments << "-jar"_L1 << signTool << "sign-app"_L1
3470 << "-mode"_L1 << "localSign"_L1
3471 << "-signAlg"_L1 << signing.signAlg << "-signCode"_L1 << "1"_L1
3472 << "-keyAlias"_L1 << signing.keyAlias << "-keystoreFile"_L1 << signing.storeFile
3473 << "-appCertFile"_L1 << signing.certPath << "-profileFile"_L1 << signing.profile
3474 << "-inFile"_L1 << *hapPath << "-outFile"_L1 << signedPath;
3475
3476 const QString apiVersion = compatibleApiVersion(options);
3477 if (apiVersion.isEmpty()) {
3478 fprintf(stderr,
3479 "Warning: cannot determine compatibleSdkVersion from build-profile.json5;"
3480 " hap-sign-tool uses its default\n");
3481 } else {
3482 arguments << "-compatibleVersion"_L1 << apiVersion;
3483 }
3484
3485 if (options.verbose) {
3486 fprintf(stdout, "Signing HAP with %s\n", qPrintable(signTool));
3487 fprintf(stdout, " Running: %s %s -keyPwd <hidden> -keystorePwd <hidden>\n",
3488 qPrintable(javaExecutable), qPrintable(arguments.join(u' ')));
3489 }
3490
3491 // hap-sign-tool only reads the passwords from argv (-pwdInputMode 1 prompts
3492 // on a console, which a build step does not have), so they are visible in a
3493 // process listing for the duration of the call.
3494 arguments << "-keyPwd"_L1 << signing.keyPassword << "-keystorePwd"_L1 << signing.storePassword;
3495
3496 QProcessExt process;
3497 process.setProcessChannelMode(QProcess::MergedChannels);
3498 process.start(javaExecutable, arguments);
3499 if (!process.waitForStarted()) {
3500 fprintf(stderr, "Error: failed to start %s\n", qPrintable(javaExecutable));
3501 return false;
3502 }
3503 // The test bundle holds every Qt library, so code signing can take long.
3504 if (!process.waitForFinished(-1)) {
3505 fprintf(stderr, "Error: hap-sign-tool did not finish\n");
3506 process.kill();
3507 return false;
3508 }
3509 if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
3510 QFile::remove(signedPath);
3511 fprintf(stderr, "Error: hap-sign-tool failed with exit code %d\n", process.exitCode());
3512 return false;
3513 }
3514 if (!QFile::exists(signedPath)) {
3515 fprintf(stderr, "Error: hap-sign-tool reported success but %s was not created\n",
3516 qPrintable(signedPath));
3517 return false;
3518 }
3519
3520 if (options.verbose)
3521 fprintf(stdout, " Signed HAP: %s\n", qPrintable(signedPath));
3522
3523 *hapPath = signedPath;
3524 return true;
3525}
3526
3527static bool buildHap(const Options &options, QString *hapOutputPath = nullptr)
3528{
3529 if (!options.buildPackage) {
3530 if (options.verbose)
3531 fprintf(stdout, "Skipping HAP build (--no-build specified)\n");
3532 return true;
3533 }
3534
3535 // Resolve hvigorw path: CLI option → env var → auto-detect in output directory
3536 QString hvigorPath = options.hvigorPath;
3537
3538 if (hvigorPath.isEmpty()) {
3539 QByteArray envHvigor = qgetenv("QT_HARMONYOS_HVIGOR");
3540 if (!envHvigor.isEmpty())
3541 hvigorPath = QString::fromLocal8Bit(envHvigor);
3542 }
3543
3544 if (hvigorPath.isEmpty()) {
3545 // Auto-detect hvigorw in the output directory (always present in the template)
3546 QString candidate = options.outputDirectory + "/hvigorw"_L1;
3547 if (QFile::exists(candidate))
3548 hvigorPath = std::move(candidate);
3549 }
3550
3551 if (hvigorPath.isEmpty()) {
3552 fprintf(stderr, "Warning: No hvigor path specified, skipping build\n");
3553 fprintf(stderr, "Use --hvigor <path> or set QT_HARMONYOS_HVIGOR\n");
3554 return true; // Not fatal
3555 }
3556
3557 if (options.verbose)
3558 fprintf(stdout, "Building HarmonyOS HAP package\n");
3559
3560 // Check if hvigorw exists
3561 if (!QFile::exists(hvigorPath)) {
3562 fprintf(stderr, "Error: hvigorw not found at: %s\n", qPrintable(hvigorPath));
3563 return false;
3564 }
3565
3566 // Determine build task and build mode. hvigor's assembleHap builds in debug
3567 // mode unless the mode is passed explicitly.
3568 QString buildTask = "assembleHap"_L1;
3569 QString buildMode = options.releaseMode ? "release"_L1 : "debug"_L1;
3570
3571 if (options.verbose) {
3572 fprintf(stdout, " Build mode: %s\n", qPrintable(buildMode));
3573 fprintf(stdout, " Running: %s %s\n", qPrintable(hvigorPath), qPrintable(buildTask));
3574 }
3575
3576 // Execute hvigorw
3577 QProcessExt process;
3578 process.setWorkingDirectory(options.outputDirectory);
3579 process.setProcessChannelMode(QProcess::MergedChannels);
3580
3581 QStringList arguments;
3582 arguments << buildTask << "-p"_L1 << ("buildMode="_L1 + buildMode);
3583 // The hvigor daemon outlives the build and, on Windows, inherits our output
3584 // pipe; QProcess then never sees EOF and this call hangs. Build daemon-less.
3585 arguments << "--no-daemon"_L1;
3586
3587 process.start(hvigorPath, arguments);
3588
3589 if (!process.waitForStarted()) {
3590 fprintf(stderr, "Failed to start hvigorw\n");
3591 return false;
3592 }
3593
3594 // Show output in real-time if verbose
3595 while (process.state() != QProcess::NotRunning) {
3596 if (!process.waitForReadyRead(1000)) {
3597 // Timeout is OK, just check if process is still running
3598 if (process.state() == QProcess::NotRunning)
3599 break;
3600 continue;
3601 }
3602
3603 if (options.verbose) {
3604 QByteArray output = process.readAll();
3605 fprintf(stdout, "%s", output.constData());
3606 fflush(stdout);
3607 }
3608 }
3609
3610 // Read any remaining output
3611 if (options.verbose) {
3612 QByteArray output = process.readAll();
3613 if (!output.isEmpty()) {
3614 fprintf(stdout, "%s", output.constData());
3615 fflush(stdout);
3616 }
3617 }
3618
3619 if (process.exitCode() != 0) {
3620 fprintf(stderr, "hvigorw failed with exit code %d\n", process.exitCode());
3621 if (!options.verbose)
3622 fprintf(stderr, "Run with --verbose to see build output\n");
3623 return false;
3624 }
3625
3626 if (options.verbose)
3627 fprintf(stdout, "HAP build completed successfully\n");
3628
3629 // Try to locate the generated HAP file
3630 if (hapOutputPath) {
3631 QString hapSearchPath = options.outputDirectory + "/entry/build/default/outputs/default"_L1;
3632 QDir hapDir(hapSearchPath);
3633
3634 if (hapDir.exists()) {
3635 QStringList hapFiles = hapDir.entryList(QStringList() << "*.hap"_L1, QDir::Files);
3636 // A signed HAP from an earlier build sorts before the unsigned one
3637 // hvigor just wrote, so pick the unsigned artifact explicitly.
3638 for (qsizetype i = 0; i < hapFiles.size(); ++i) {
3639 if (hapFiles.at(i).endsWith("-unsigned.hap"_L1)) {
3640 hapFiles.move(i, 0);
3641 break;
3642 }
3643 }
3644 if (!hapFiles.isEmpty()) {
3645 *hapOutputPath = hapDir.absoluteFilePath(hapFiles.first());
3646 if (options.verbose)
3647 fprintf(stdout, " Generated HAP: %s\n", qPrintable(*hapOutputPath));
3648 }
3649 }
3650 }
3651
3652 return true;
3653}
3654
3655static bool installToDevice(const Options &options, const QString &hapPath)
3656{
3657 if (!options.installApk)
3658 return true; // Not requested
3659
3660 if (hapPath.isEmpty()) {
3661 fprintf(stderr, "Error: Cannot install - HAP file path not found\n");
3662 return false;
3663 }
3664
3665 if (options.verbose)
3666 fprintf(stdout, "Installing HAP to device\n");
3667
3668 // Check if hdc is available
3669 QString hdcPath = "hdc"_L1; // Assume it's in PATH
3670
3671 // Try to find hdc in SDK
3672 if (!options.sdkRoot.isEmpty()) {
3673 QString sdkHdc = options.sdkRoot + "/command-line-tools/hdc"_L1;
3674 if (QFile::exists(sdkHdc))
3675 hdcPath = std::move(sdkHdc);
3676 }
3677
3678 // Check for connected devices
3679 QProcessExt checkDevices;
3680 checkDevices.start(hdcPath, QStringList() << "list"_L1 << "targets"_L1);
3681 if (!checkDevices.waitForFinished(5000)) {
3682 fprintf(stderr, "Error: Failed to check for connected devices\n");
3683 return false;
3684 }
3685
3686 QString devicesOutput = QString::fromUtf8(checkDevices.readAllStandardOutput());
3687 if (devicesOutput.trimmed().isEmpty() || devicesOutput.contains("empty"_L1)) {
3688 fprintf(stderr, "Error: No HarmonyOS devices connected\n");
3689 fprintf(stderr, "Connect a device and ensure USB debugging is enabled\n");
3690 return false;
3691 }
3692
3693 if (options.verbose)
3694 fprintf(stdout, " Connected devices:\n%s\n", qPrintable(devicesOutput));
3695
3696 // Uninstall old version if exists
3697 if (options.verbose)
3698 fprintf(stdout, " Uninstalling old version (if exists)\n");
3699
3700 QProcessExt uninstall;
3701 uninstall.start(hdcPath, QStringList() << "uninstall"_L1 << options.harmonyOsAppBundleName);
3702 uninstall.waitForFinished(10000);
3703 // Don't check result - it's OK if app wasn't installed
3704
3705 // Install new HAP
3706 if (options.verbose)
3707 fprintf(stdout, " Installing: %s\n", qPrintable(hapPath));
3708
3709 QProcessExt install;
3710 install.setProcessChannelMode(QProcess::MergedChannels);
3711 install.start(hdcPath, QStringList() << "install"_L1 << hapPath);
3712
3713 if (!install.waitForFinished(60000)) { // 60 second timeout
3714 fprintf(stderr, "Error: Installation timed out\n");
3715 return false;
3716 }
3717
3718 QString installOutput = QString::fromUtf8(install.readAll());
3719
3720 if (install.exitCode() != 0) {
3721 fprintf(stderr, "Error: Installation failed\n");
3722 fprintf(stderr, "%s\n", qPrintable(installOutput));
3723 return false;
3724 }
3725
3726 if (options.verbose)
3727 fprintf(stdout, " Installation output:\n%s\n", qPrintable(installOutput));
3728
3729 // Launch the app
3730 if (options.verbose)
3731 fprintf(stdout, " Launching application\n");
3732
3733 QProcessExt launch;
3734 QStringList launchArgs;
3735 launchArgs << "shell"_L1 << "aa"_L1 << "start"_L1
3736 << "-a"_L1 << "EntryAbility"_L1
3737 << "-b"_L1 << options.harmonyOsAppBundleName;
3738
3739 launch.start(hdcPath, launchArgs);
3740 launch.waitForFinished(5000);
3741
3742 if (launch.exitCode() != 0) {
3743 fprintf(stderr, "Warning: Failed to launch application\n");
3744 // Not fatal
3745 }
3746
3747 fprintf(stdout, "Successfully installed and launched application on device\n");
3748
3749 return true;
3750}
3751
3752int main(int argc, char *argv[])
3753{
3754 QCoreApplication app(argc, argv);
3755 QCoreApplication::setApplicationName("harmonydeployqt"_L1);
3756 QCoreApplication::setApplicationVersion("1.0"_L1);
3757
3758 Options options;
3759 options.timer.start();
3760
3761 if (!parseCommandLine(app.arguments(), &options))
3762 return 1;
3763
3764 if (options.verbose) {
3765 fprintf(stdout, "Qt HarmonyOS Deployment Tool\n");
3766 fprintf(stdout, "==============================\n\n");
3767 }
3768
3769 if (!readInputConfiguration(&options))
3770 return 1;
3771
3772 // Resolve before anything is generated: bad signing input should not cost a
3773 // full HAP build to report.
3774 SigningConfig signing;
3775 if (!resolveSigningConfig(options, &signing))
3776 return 1;
3777
3778 const QList<TestQmlModule> testQmlModules =
3779 options.testBundleMode ? findTestQmlModules(options) : QList<TestQmlModule>{};
3780
3781 if (options.testBundleMode && !verifyUniqueTestQmlModuleDeployDirs(testQmlModules))
3782 return 1;
3783
3784 if (options.verbose)
3785 fprintf(stdout, "\nDeployment process started...\n");
3786
3787 QString hapOutputPath;
3788
3789 // Phase 1: Copy template
3790 if (!copyTemplate(options)) {
3791 fprintf(stderr, "Failed to copy template\n");
3792 return 1;
3793 }
3794
3795 // Phase 2: Customize template (in test bundle mode, APP_LIBRARY_NAME uses a placeholder)
3796 if (!customizeTemplate(options)) {
3797 fprintf(stderr, "Failed to customize template\n");
3798 return 1;
3799 }
3800
3801 // Phase 3: Copy libraries / dependencies (mode-specific)
3802 QStringList bundledBinaries; // populated only in test bundle mode
3803 if (options.testBundleMode) {
3804 if (!copyTestBinaries(options, bundledBinaries)) {
3805 fprintf(stderr, "Failed to copy test binaries\n");
3806 return 1;
3807 }
3808 if (!copyAllQtLibs(options)) {
3809 fprintf(stderr, "Failed to copy Qt libraries\n");
3810 return 1;
3811 }
3812 if (!copyAllQtPlugins(options)) {
3813 fprintf(stderr, "Failed to copy Qt plugins\n");
3814 return 1;
3815 }
3816 if (!copyAllQmlModules(options)) {
3817 fprintf(stderr, "Failed to copy QML modules\n");
3818 return 1;
3819 }
3820 if (!copyTestQmlModules(testQmlModules, options)) {
3821 fprintf(stderr, "Failed to copy test QML modules\n");
3822 return 1;
3823 }
3824 } else {
3825 if (!copyApplicationBinary(options)) {
3826 fprintf(stderr, "Failed to copy application binary\n");
3827 return 1;
3828 }
3829 if (!copyProjectLibraries(options)) {
3830 fprintf(stderr, "Failed to copy project libraries\n");
3831 return 1;
3832 }
3833 QSet<QString> processedLibs;
3834 if (!detectAndCopyDependencies(options, processedLibs)) {
3835 fprintf(stderr, "Failed to detect and copy dependencies\n");
3836 return 1;
3837 }
3838 bool qmlScanOk = true;
3839 QList<QmlImportInfo> qmlImports = scanQmlImports(options, &qmlScanOk);
3840 if (!qmlScanOk) {
3841 fprintf(stderr, "Failed to scan QML imports\n");
3842 return 1;
3843 }
3844 if (!copyQmlFiles(options)) {
3845 fprintf(stderr, "Failed to copy QML files\n");
3846 return 1;
3847 }
3848 if (!copyQmlImports(options, qmlImports, processedLibs)) {
3849 fprintf(stderr, "Failed to copy QML imports\n");
3850 return 1;
3851 }
3852 if (!copyPlugins(options, processedLibs)) {
3853 fprintf(stderr, "Failed to copy plugins\n");
3854 return 1;
3855 }
3856 }
3857
3858 if (!copyExtraPlugins(options)) {
3859 fprintf(stderr, "Failed to copy extra plugins\n");
3860 return 1;
3861 }
3862
3863 if (!injectSigningConfig(options, signing)) {
3864 fprintf(stderr, "Failed to inject signing configuration\n");
3865 return 1;
3866 }
3867
3868 // Phase 4: Build HAP package
3869 if (!buildHap(options, &hapOutputPath)) {
3870 fprintf(stderr, "Failed to build HAP\n");
3871 return 1;
3872 }
3873
3874 if (!signHap(options, signing, &hapOutputPath)) {
3875 fprintf(stderr, "Failed to sign HAP\n");
3876 return 1;
3877 }
3878
3879 // Phase 5: Test bundle finalization
3880 if (options.testBundleMode) {
3881 if (!writeTestBinariesList(options, bundledBinaries)) {
3882 fprintf(stderr, "Failed to write binaries.txt\n");
3883 return 1;
3884 }
3885 }
3886
3887 // hvigor writes the unsigned HAP whether or not it also signs, so that is the
3888 // OUTPUT of the CMake custom command.
3889 if (!options.depFilePath.isEmpty()) {
3890 const QString unsignedHapPath = options.outputDirectory
3891 + "/entry/build/default/outputs/default/entry-default-unsigned.hap"_L1;
3892 if (!writeDepfile(options, unsignedHapPath))
3893 fprintf(stderr, "Warning: Failed to write dependency file\n");
3894 }
3895
3896 // Phase 6: Install to device (standard mode only)
3897 if (!options.testBundleMode) {
3898 if (!installToDevice(options, hapOutputPath)) {
3899 fprintf(stderr, "Failed to install to device\n");
3900 return 1;
3901 }
3902 }
3903
3904 fprintf(stdout, "\n==============================================\n");
3905 fprintf(stdout, "Deployment completed successfully!\n");
3906 fprintf(stdout, "==============================================\n");
3907 fprintf(stdout, "Project location: %s\n", qPrintable(options.outputDirectory));
3908
3909 if (!hapOutputPath.isEmpty())
3910 fprintf(stdout, "HAP package: %s\n", qPrintable(hapOutputPath));
3911
3912 if (options.verbose)
3913 fprintf(stdout, "\nTotal time: %lld ms\n", options.timer.elapsed());
3914
3915 return 0;
3916}
QProcessExt()
Definition main.cpp:152
static QStringList dependenciesForDepfile
Definition main.cpp:49
static bool copyTemplate(const Options &options)
Definition main.cpp:675
static bool copyRecursively(const QString &sourceDir, const QString &destDir, bool verbose, const QString &topDest={})
Definition main.cpp:630
static bool customizeTemplate(const Options &options)
Definition main.cpp:801
static bool readInputConfiguration(Options *options)
Definition main.cpp:286
static bool writeDepfile(const Options &options, const QString &hapOutputPath)
Definition main.cpp:589
static bool writeTestBinariesList(const Options &options, const QStringList &bundledBinaries)
Definition main.cpp:1556
static bool getTestQmlModuleDeployDir(const TestQmlModule &testModule, QString &deployDir)
Definition main.cpp:1957
static bool copyFileIfNewer(const QString &sourceFileName, const QString &destinationFileName, bool verbose, bool forceOverwrite=false)
Definition main.cpp:543
static QString findJavaExecutable()
Definition main.cpp:3343
static void scanTestBinariesDir(const QString &dirPath, const QStringList &excludeList, const QStringList &excludeDirs, QStringList &found, QStringList &foundHelpers, QSet< QString > &helperNames)
Definition main.cpp:1425
static bool installToDevice(const Options &options, const QString &hapPath)
Definition main.cpp:3655
static QString findNearestEnclosingTestDir(const QString &startDir, const QSet< QString > &testDirs)
Definition main.cpp:1938
static QString sdkVersion(const QString &sdkRoot)
Definition main.cpp:264
static bool buildHap(const Options &options, QString *hapOutputPath=nullptr)
Definition main.cpp:3527
static bool copyApplicationBinary(const Options &options)
Definition main.cpp:1267
static bool readQmldirLines(const QString &qmldirPath, QStringList &lines)
Definition main.cpp:1865
static QList< TestQmlModule > findTestQmlModules(const Options &options)
Definition main.cpp:1973
static QString jsonStringEscape(const QString &s)
Definition main.cpp:764
static bool copyTestBinaries(const Options &options, QStringList &bundledBinaries)
Definition main.cpp:1467
static QString findQtPluginsDirectory(const Options &options)
Definition main.cpp:2399
static QString findHapSignTool(const Options &options)
Definition main.cpp:3360
static bool copyAllQtLibs(const Options &options)
Definition main.cpp:1586
static QStringList readElfDependencies(const Options &options, const QString &binaryPath)
Definition main.cpp:2133
static bool copyFileToArchitectures(const Options &options, const QString &sourcePath, const QString &relativeDestPath, bool trackInDepfile=true)
Definition main.cpp:1319
static bool getQmldirModuleUri(const QString &qmldirPath, QString &uri)
Definition main.cpp:1918
static bool copyQmlImports(const Options &options, const QList< QmlImportInfo > &imports, QSet< QString > &processedLibs)
Definition main.cpp:2880
static bool reasonNeedsPromotion(const QString &reason)
Definition main.cpp:741
static bool parseCommandLine(const QStringList &arguments, Options *options)
Definition main.cpp:166
static bool resolveSigningConfig(const Options &options, SigningConfig *signing)
Definition main.cpp:3124
static QString findStdCppLibrary(const Options &options, const QString &arch)
Definition main.cpp:1347
static bool copyTestQmlModules(const QList< TestQmlModule > &modules, const Options &options)
Definition main.cpp:2068
static bool signHap(const Options &options, const SigningConfig &signing, QString *hapPath)
Definition main.cpp:3419
static void printHelp()
Definition main.cpp:112
static bool copyExtraPlugins(const Options &options)
Definition main.cpp:1747
static bool copyPlugins(const Options &options, QSet< QString > &processedLibs)
Definition main.cpp:2517
static QString findQtLibrary(const Options &options, const QString &libName)
Definition main.cpp:2223
static bool copyQmlFiles(const Options &options)
Definition main.cpp:2842
static bool copyAllQtPlugins(const Options &options)
Definition main.cpp:1664
static bool copyAllQmlModules(const Options &options)
Definition main.cpp:1824
static QString findLlvmReadobj(const Options &options)
Definition main.cpp:2085
static bool injectSigningConfig(const Options &options, const SigningConfig &signing)
Definition main.cpp:3257
static bool copyTestQmlModuleFiles(const QString &moduleSrcDirPath, const QString &destModuleDirPath, const Options &options)
Definition main.cpp:2033
static QString synthesizePermissionReasonId(const QString &permissionName)
Definition main.cpp:753
static bool isValidHarmonyOsAbilityOrientation(const QString &value)
Definition main.cpp:774
static QString hapQmlDir(const Options &options)
Definition main.cpp:1819
static bool verifyUniqueTestQmlModuleDeployDirs(const QList< TestQmlModule > &modules)
Definition main.cpp:2010
static bool copyPlatformPlugin(const Options &options, const QString &qtPluginsPath, QSet< QString > &processedLibs)
Definition main.cpp:2427
static QString compatibleApiVersion(const Options &options)
Definition main.cpp:3387
static bool detectAndCopyDependencies(const Options &options, QSet< QString > &processedLibs)
Definition main.cpp:2257
static bool copyProjectLibraries(const Options &options)
Definition main.cpp:1378
static QString readElfSoname(const Options &options, const QString &binaryPath)
Definition main.cpp:2109
static bool isSystemLibrary(const QString &libName)
Definition main.cpp:2210
static bool copyQmldirPlugins(const QString &qmldirPath, const Options &options)
Definition main.cpp:1880
static QList< QmlImportInfo > scanQmlImports(const Options &options, bool *ok=nullptr)
Definition main.cpp:2619
static QString findExtraDepLibrary(const Options &options, const QString &libName)
Definition main.cpp:2190
static bool copyQmlDir(const QString &srcDir, const QString &relPath, const QString &qmlDestBase, const Options &options)
Definition main.cpp:1783
int main(int argc, char *argv[])
[ctor_close]
QString signingCertPath
Definition main.cpp:99
bool installApk
Definition main.cpp:224
QString harmonyOsTargetSdkVersion
Definition main.cpp:81
QString harmonyOsAppVendor
Definition main.cpp:71
bool releaseMode
Definition main.cpp:58
QString testBinariesDirectory
Definition main.cpp:95
QStringList permissions
Definition main.cpp:64
QString signingPlainStorePassword
Definition main.cpp:106
QString harmonyOsAppBundleName
Definition main.cpp:41
QString sdkRoot
Definition main.cpp:42
QString signingAlg
Definition main.cpp:107
QString inputFile
Definition main.cpp:34
QStringList targetArchs
Definition main.cpp:47
QStringList qmlImportPaths
Definition main.cpp:169
QStringList qmlRootPaths
Definition main.cpp:44
QString harmonyOsModuleDescription
Definition main.cpp:89
QString qtHostDirectory
Definition main.cpp:154
QString qtLibExecsDirectory
Definition main.cpp:151
QStringList projectLibraries
Definition main.cpp:38
int harmonyOsAppVersionCode
Definition main.cpp:72
QString depFilePath
Definition main.cpp:167
bool testBundleMode
Definition main.cpp:94
QString qtLibsDirectory
Definition main.cpp:150
QStringList testExcludeList
Definition main.cpp:96
QString harmonyOsPackageSourceDirectory
Definition main.cpp:39
QString harmonyOsAppLabel
Definition main.cpp:74
QStringList harmonyOsModuleDeviceTypes
Definition main.cpp:90
QStringList extraLibsDirs
Definition main.cpp:55
QString harmonyOsCompatibleSdkVersion
Definition main.cpp:80
QString signingProfile
Definition main.cpp:100
QString hvigorPath
Definition main.cpp:36
QString signingKeyAlias
Definition main.cpp:102
QStringList pluginsImportPaths
Definition main.cpp:46
bool verbose
Definition main.cpp:130
QStringList extraPlugins
Definition main.cpp:200
QString signingKeyPassword
Definition main.cpp:103
QString harmonyOsAppVersionName
Definition main.cpp:73
QString depFileBase
Definition main.cpp:63
QString qtPluginsDirectory
Definition main.cpp:152
QString harmonyOsAbilityOrientation
Definition main.cpp:91
QString qtQmlDirectory
Definition main.cpp:153
QString outputDirectory
Definition main.cpp:161
QString applicationBinary
Definition main.cpp:163
QString harmonyOsAppName
Definition main.cpp:40
QString signingStorePassword
Definition main.cpp:104
QString harmonyOsCompileSdkVersion
Definition main.cpp:82
QString harmonyOsAppIcon
Definition main.cpp:75
QString signingStoreFile
Definition main.cpp:101
QString signingPlainKeyPassword
Definition main.cpp:105
QElapsedTimer timer
Definition main.cpp:136
bool buildPackage
Definition main.cpp:60
QString ndkRoot
Definition main.cpp:43
QString value
Definition main.cpp:798
QString id
Definition main.cpp:797
bool pluginIsOptional
Definition main.cpp:2613
QString path
Definition main.cpp:2610
QString prefer
Definition main.cpp:2614
QString plugin
Definition main.cpp:2612
QString name
Definition main.cpp:2609
QStringList scripts
Definition main.cpp:2616
QStringList components
Definition main.cpp:2615
QString type
Definition main.cpp:2611
QString absolutePath
Definition main.cpp:73
QString relativePath
Definition main.cpp:72
QString storePassword
Definition main.cpp:3118
QString certPath
Definition main.cpp:3113
QString keyAlias
Definition main.cpp:3116
bool encryptedPasswords
Definition main.cpp:3112
QString profile
Definition main.cpp:3114
QString keyPassword
Definition main.cpp:3117
QString signAlg
Definition main.cpp:3119
QString storeFile
Definition main.cpp:3115
QString qmldirPath
Definition main.cpp:1953
QString testDir
Definition main.cpp:1954