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