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