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