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