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