Branch data Line data Source code
1 : : // Copyright (C) 2021 The Qt Company Ltd.
2 : : // Copyright (C) 2019 Luxoft Sweden AB
3 : : // Copyright (C) 2018 Pelagicore AG
4 : : // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
5 : :
6 : : #include <memory>
7 : : #include <stdio.h>
8 : :
9 : : #include <QCoreApplication>
10 : : #include <QCommandLineParser>
11 : : #include <QStringList>
12 : : #include <QFile>
13 : : #include <QJsonArray>
14 : : #include <QJsonDocument>
15 : : #include <QDebug>
16 : :
17 : : #include <QtAppManCommon/exception.h>
18 : : #include <QtAppManCommon/qtyaml.h>
19 : : #include <QtAppManCommon/utilities.h>
20 : : #include <QtAppManCrypto/cryptography.h>
21 : : #include <QtAppManPackage/packageutilities.h>
22 : : #include "packagingjob.h"
23 : :
24 : : using namespace Qt::StringLiterals;
25 : :
26 : :
27 : : QT_USE_NAMESPACE_AM
28 : :
29 : : enum Command {
30 : : NoCommand,
31 : : CreatePackage,
32 : : DevSignPackage,
33 : : DevVerifyPackage,
34 : : StoreSignPackage,
35 : : StoreVerifyPackage,
36 : : YamlToJson
37 : : };
38 : :
39 : : // REMEMBER to update the completion file util/bash/appman-prompt, if you apply changes below!
40 : : static struct {
41 : : Command command;
42 : : const char *name;
43 : : const char *description;
44 : : } commandTable[] = {
45 : : { CreatePackage, "create-package", "Create a new package." },
46 : : { DevSignPackage, "dev-sign-package", "Add developer signature to package." },
47 : : { DevVerifyPackage, "dev-verify-package", "Verify developer signature on package." },
48 : : { StoreSignPackage, "store-sign-package", "Add store signature to package." },
49 : : { StoreVerifyPackage, "store-verify-package", "Verify store signature on package." },
50 : : { YamlToJson, "yaml-to-json", "Convenience functionality for build systems (internal)." }
51 : : };
52 : :
53 : 0 : static Command command(QCommandLineParser &clp)
54 : : {
55 [ # # ]: 0 : if (!clp.positionalArguments().isEmpty()) {
56 [ # # ]: 0 : QByteArray cmd = clp.positionalArguments().at(0).toLatin1();
57 : :
58 [ # # ]: 0 : for (uint i = 0; i < sizeof(commandTable) / sizeof(commandTable[0]); ++i) {
59 [ # # ]: 0 : if (cmd == commandTable[i].name) {
60 [ # # ]: 0 : clp.clearPositionalArguments();
61 [ # # # # ]: 0 : clp.addPositionalArgument(QString::fromLatin1(cmd),
62 [ # # ]: 0 : QString::fromLatin1(commandTable[i].description),
63 [ # # ]: 0 : QString::fromLatin1(cmd));
64 : 0 : return commandTable[i].command;
65 : : }
66 : : }
67 : 0 : }
68 : : return NoCommand;
69 : : }
70 : :
71 : 0 : int main(int argc, char *argv[])
72 : : {
73 : : // enable OpenSSL3 to load old certificates
74 : 0 : Cryptography::enableOpenSsl3LegacyProvider();
75 : :
76 [ # # ]: 0 : QCoreApplication::setApplicationName(u"Qt ApplicationManager Packager"_s);
77 [ # # ]: 0 : QCoreApplication::setOrganizationName(u"QtProject"_s);
78 [ # # ]: 0 : QCoreApplication::setOrganizationDomain(u"qt-project.org"_s);
79 [ # # ]: 0 : QCoreApplication::setApplicationVersion(QString::fromLatin1(QT_AM_VERSION_STR));
80 : :
81 : 0 : QCoreApplication a(argc, argv);
82 : :
83 [ # # ]: 0 : QByteArray desc = "\n\nAvailable commands are:\n";
84 : 0 : size_t longestName = 0;
85 [ # # ]: 0 : for (uint i = 0; i < sizeof(commandTable) / sizeof(commandTable[0]); ++i)
86 [ # # # # ]: 0 : longestName = qMax(longestName, qstrlen(commandTable[i].name));
87 [ # # ]: 0 : for (uint i = 0; i < sizeof(commandTable) / sizeof(commandTable[0]); ++i) {
88 [ # # ]: 0 : desc += " ";
89 [ # # ]: 0 : desc += commandTable[i].name;
90 [ # # # # ]: 0 : desc += QByteArray(1 + qsizetype(longestName - qstrlen(commandTable[i].name)), ' ');
91 [ # # ]: 0 : desc += commandTable[i].description;
92 [ # # ]: 0 : desc += '\n';
93 : : }
94 : :
95 [ # # ]: 0 : desc += "\nMore information about each command can be obtained by running\n" \
96 : 0 : " appman-packager <command> --help";
97 : :
98 [ # # ]: 0 : QCommandLineParser clp;
99 [ # # # # : 0 : clp.setApplicationDescription(u"\n"_s + QCoreApplication::applicationName() + QString::fromLatin1(desc));
# # # # ]
100 : :
101 [ # # ]: 0 : clp.addHelpOption();
102 [ # # ]: 0 : clp.addVersionOption();
103 : :
104 [ # # ]: 0 : clp.addPositionalArgument(u"command"_s, u"The command to execute."_s);
105 : :
106 : : // ignore unknown options for now -- the sub-commands may need them later
107 [ # # ]: 0 : clp.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments);
108 : :
109 [ # # # # : 0 : if (!clp.parse(QCoreApplication::arguments())) {
# # ]
110 [ # # # # : 0 : fprintf(stderr, "%s\n", qPrintable(clp.errorText()));
# # ]
111 : 0 : exit(1);
112 : : }
113 [ # # ]: 0 : clp.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsOptions);
114 : :
115 : 0 : try {
116 : 0 : std::unique_ptr<PackagingJob> p;
117 : :
118 : : // REMEMBER to update the completion file util/bash/appman-prompt, if you apply changes below!
119 [ # # # # : 0 : switch (command(clp)) {
# # # #
# ]
120 : 0 : default:
121 : 0 : case NoCommand:
122 [ # # # # ]: 0 : if (clp.isSet(u"version"_s))
123 : 0 : clp.showVersion();
124 [ # # # # ]: 0 : if (clp.isSet(u"help"_s))
125 : 0 : clp.showHelp();
126 : 0 : clp.showHelp(1);
127 : 0 : break;
128 : :
129 : 0 : case CreatePackage: {
130 [ # # # # ]: 0 : clp.addOption({ u"verbose"_s, u"Dump the package's meta-data header and footer information to stdout."_s });
131 [ # # # # ]: 0 : clp.addOption({ u"json"_s, u"Output in JSON format instead of YAML."_s });
132 [ # # # # : 0 : clp.addOption({{ u"extra-metadata"_s, u"m"_s }, u"Add extra meta-data to the package, supplied on the command line."_s, u"yaml-snippet"_s });
# # # # ]
133 [ # # # # : 0 : clp.addOption({{ u"extra-metadata-file"_s, u"M"_s }, u"Add extra meta-data to the package, read from file."_s, u"yaml-file"_s });
# # # # ]
134 [ # # # # : 0 : clp.addOption({{ u"extra-signed-metadata"_s, u"s"_s }, u"Add extra, digitally signed, meta-data to the package, supplied on the command line."_s, u"yaml-snippet"_s });
# # # # ]
135 [ # # # # : 0 : clp.addOption({{ u"extra-signed-metadata-file"_s, u"S"_s }, u"Add extra, digitally signed, meta-data to the package, read from file."_s, u"yaml-file"_s });
# # # # ]
136 [ # # ]: 0 : clp.addPositionalArgument(u"package"_s, u"The file name of the created package."_s);
137 [ # # ]: 0 : clp.addPositionalArgument(u"source-directory"_s, u"The package's content root directory."_s);
138 [ # # ]: 0 : clp.process(a);
139 : :
140 [ # # # # ]: 0 : if (clp.positionalArguments().size() != 3)
141 : 0 : clp.showHelp(1);
142 : :
143 : 0 : auto parseYamlMetada = [](const QStringList &metadataSnippets, const QStringList &metadataFiles, bool isSigned) -> QVariantMap {
144 : 0 : QVariantMap result;
145 : 0 : QVector<QPair<QByteArray, QString>> metadata;
146 : :
147 [ # # ]: 0 : for (const QString &file : metadataFiles) {
148 [ # # ]: 0 : QFile f(file);
149 [ # # # # ]: 0 : if (!f.open(QIODevice::ReadOnly))
150 : 0 : throw Exception(f, "Could not open metadata file for reading");
151 [ # # # # ]: 0 : metadata.append(qMakePair(f.readAll(), file));
152 : 0 : }
153 [ # # ]: 0 : for (const QString &snippet : metadataSnippets)
154 [ # # # # ]: 0 : metadata.append(qMakePair(snippet.toUtf8(), QString()));
155 : :
156 [ # # # # : 0 : for (const auto &md : metadata) {
# # ]
157 : 0 : QVector<QVariant> docs;
158 : 0 : try {
159 [ # # ]: 0 : docs = YamlParser::parseAllDocuments(md.first);
160 [ - - ]: 0 : } catch (const Exception &e) {
161 : 0 : throw Exception(Error::IO, "in --extra-%1metadata%2 %3: %4")
162 [ - - - - ]: 0 : .arg(isSigned ? "signed-" : "").arg(md.second.isEmpty() ? "": "-file")
163 [ - - ]: 0 : .arg(md.second.isEmpty() ? u"option"_s : md.second)
164 : 0 : .arg(e.errorString());
165 : 0 : }
166 [ # # ]: 0 : if (docs.size() < 1) {
167 : 0 : throw Exception("Could not parse --extra-%1metadata%2 %3: Invalid document format")
168 [ # # # # ]: 0 : .arg(isSigned ? "signed-" : "").arg(md.second.isEmpty() ? "": "-file")
169 [ # # ]: 0 : .arg(md.second.isEmpty() ? u"option"_s : md.second);
170 : : }
171 [ # # # # : 0 : for (const auto &doc : docs)
# # ]
172 [ # # # # ]: 0 : recursiveMergeVariantMap(result, doc.toMap());
173 : 0 : }
174 : 0 : return result;
175 : 0 : };
176 : :
177 [ # # ]: 0 : QVariantMap extraMetaDataMap = parseYamlMetada(clp.values(u"extra-metadata"_s),
178 [ # # ]: 0 : clp.values(u"extra-metadata-file"_s),
179 [ # # ]: 0 : false);
180 [ # # ]: 0 : QVariantMap extraSignedMetaDataMap = parseYamlMetada(clp.values(u"extra-signed-metadata"_s),
181 [ # # ]: 0 : clp.values(u"extra-signed-metadata-file"_s),
182 [ # # ]: 0 : true);
183 : :
184 [ # # # # ]: 0 : p.reset(PackagingJob::create(clp.positionalArguments().at(1),
185 [ # # # # ]: 0 : clp.positionalArguments().at(2),
186 : : extraMetaDataMap,
187 : : extraSignedMetaDataMap,
188 [ # # ]: 0 : clp.isSet(u"json"_s)));
189 : 0 : break;
190 : 0 : }
191 : 0 : case DevSignPackage:
192 [ # # # # ]: 0 : clp.addOption({ u"verbose"_s, u"Dump the package's meta-data header and footer information to stdout."_s });
193 [ # # # # ]: 0 : clp.addOption({ u"json"_s, u"Output in JSON format instead of YAML."_s });
194 [ # # ]: 0 : clp.addPositionalArgument(u"package"_s, u"File name of the unsigned package (input)."_s);
195 [ # # ]: 0 : clp.addPositionalArgument(u"signed-package"_s, u"File name of the signed package (output)."_s);
196 [ # # ]: 0 : clp.addPositionalArgument(u"certificate"_s, u"PKCS#12 certificate file."_s);
197 [ # # ]: 0 : clp.addPositionalArgument(u"password"_s, u"Password for the PKCS#12 certificate."_s);
198 [ # # ]: 0 : clp.process(a);
199 : :
200 [ # # # # ]: 0 : if (clp.positionalArguments().size() != 5)
201 : 0 : clp.showHelp(1);
202 : :
203 [ # # # # ]: 0 : p.reset(PackagingJob::developerSign(clp.positionalArguments().at(1),
204 [ # # ]: 0 : clp.positionalArguments().at(2),
205 [ # # ]: 0 : clp.positionalArguments().at(3),
206 [ # # # # ]: 0 : clp.positionalArguments().at(4),
207 [ # # ]: 0 : clp.isSet(u"json"_s)));
208 : 0 : break;
209 : :
210 : 0 : case DevVerifyPackage:
211 [ # # # # ]: 0 : clp.addOption({ u"verbose"_s, u"Print details regarding the verification to stdout."_s });
212 [ # # ]: 0 : clp.addPositionalArgument(u"package"_s, u"File name of the signed package (input)."_s);
213 [ # # ]: 0 : clp.addPositionalArgument(u"certificates"_s, u"The developer's CA certificate file(s)."_s, u"certificates..."_s);
214 [ # # ]: 0 : clp.process(a);
215 : :
216 [ # # # # ]: 0 : if (clp.positionalArguments().size() < 3)
217 : 0 : clp.showHelp(1);
218 : :
219 [ # # # # : 0 : p.reset(PackagingJob::developerVerify(clp.positionalArguments().at(1),
# # ]
220 [ # # ]: 0 : clp.positionalArguments().mid(2)));
221 : 0 : break;
222 : :
223 : 0 : case StoreSignPackage:
224 [ # # # # ]: 0 : clp.addOption({ u"verbose"_s, u"Dump the package's meta-data header and footer information to stdout."_s });
225 [ # # # # ]: 0 : clp.addOption({ u"json"_s, u"Output in JSON format instead of YAML."_s });
226 [ # # ]: 0 : clp.addPositionalArgument(u"package"_s, u"File name of the unsigned package (input)."_s);
227 [ # # ]: 0 : clp.addPositionalArgument(u"signed-package"_s, u"File name of the signed package (output)."_s);
228 [ # # ]: 0 : clp.addPositionalArgument(u"certificate"_s, u"PKCS#12 certificate file."_s);
229 [ # # ]: 0 : clp.addPositionalArgument(u"password"_s, u"Password for the PKCS#12 certificate."_s);
230 [ # # ]: 0 : clp.addPositionalArgument(u"hardware-id"_s, u"Unique hardware id to which this package gets bound."_s);
231 [ # # ]: 0 : clp.process(a);
232 : :
233 [ # # # # ]: 0 : if (clp.positionalArguments().size() != 6)
234 : 0 : clp.showHelp(1);
235 : :
236 [ # # # # ]: 0 : p.reset(PackagingJob::storeSign(clp.positionalArguments().at(1),
237 [ # # ]: 0 : clp.positionalArguments().at(2),
238 [ # # ]: 0 : clp.positionalArguments().at(3),
239 [ # # ]: 0 : clp.positionalArguments().at(4),
240 [ # # # # ]: 0 : clp.positionalArguments().at(5),
241 [ # # ]: 0 : clp.isSet(u"json"_s)));
242 : 0 : break;
243 : :
244 : 0 : case StoreVerifyPackage:
245 [ # # # # ]: 0 : clp.addOption({ u"verbose"_s, u"Print details regarding the verification to stdout."_s });
246 [ # # ]: 0 : clp.addPositionalArgument(u"package"_s, u"File name of the signed package (input)."_s);
247 [ # # ]: 0 : clp.addPositionalArgument(u"certificates"_s, u"Store CA certificate file(s)."_s, u"certificates..."_s);
248 [ # # ]: 0 : clp.addPositionalArgument(u"hardware-id"_s, u"Unique hardware id to which this package was bound."_s);
249 [ # # ]: 0 : clp.process(a);
250 : :
251 [ # # # # ]: 0 : if (clp.positionalArguments().size() < 4)
252 : 0 : clp.showHelp(1);
253 : :
254 [ # # # # : 0 : p.reset(PackagingJob::storeVerify(clp.positionalArguments().at(1),
# # ]
255 [ # # # # ]: 0 : clp.positionalArguments().mid(2, clp.positionalArguments().size() - 3),
256 [ # # # # ]: 0 : *--clp.positionalArguments().cend()));
257 : 0 : break;
258 : :
259 : 0 : case YamlToJson: {
260 [ # # # # : 0 : clp.addOption({{ u"i"_s, u"document-index"_s }, u"Only output the specified YAML sub-document."_s, u"index"_s });
# # # # ]
261 [ # # ]: 0 : clp.addPositionalArgument(u"yaml-file"_s, u"YAML file name, defaults to stdin (input)."_s);
262 [ # # ]: 0 : clp.process(a);
263 : :
264 [ # # # # ]: 0 : if (clp.positionalArguments().size() > 2)
265 : 0 : clp.showHelp(1);
266 : :
267 [ # # # # ]: 0 : QString yamlName = (clp.positionalArguments().size() == 2) ? clp.positionalArguments().at(1)
268 [ # # ]: 0 : : u"-"_s;
269 [ # # ]: 0 : QFile yaml(yamlName);
270 [ # # ]: 0 : if (yamlName == u"-") {
271 [ # # # # ]: 0 : if (!yaml.open(0, QIODevice::ReadOnly))
272 : 0 : throw Exception("Could not open stdin for reading");
273 [ # # # # ]: 0 : } else if (!yaml.open(QIODevice::ReadOnly)) {
274 : 0 : throw Exception(yaml, "Could not open YAML input file");
275 : : }
276 : :
277 [ # # # # ]: 0 : QVector<QVariant> docs = YamlParser::parseAllDocuments(yaml.readAll());
278 [ # # ]: 0 : QJsonDocument json;
279 [ # # # # ]: 0 : if (clp.isSet(u"i"_s)) {
280 : 0 : bool isInt;
281 [ # # ]: 0 : int index = clp.value(u"i"_s).toInt(&isInt);
282 [ # # # # ]: 0 : if (!isInt || index < 0) {
283 [ # # ]: 0 : throw Exception("Invalid document index specified: %1").arg(clp.value(u"i"_s));
284 [ # # ]: 0 : } else if (index >= docs.size()) {
285 : 0 : throw Exception("Requested YAML sub document at index %1, but only indices 0 to %2 are available in this document.")
286 : 0 : .arg(index).arg(docs.size() - 1);
287 : : } else {
288 [ # # ]: 0 : json = QJsonDocument::fromVariant(docs.at(index));
289 : : }
290 : : } else {
291 [ # # # # ]: 0 : json.setArray(QJsonArray::fromVariantList(docs.toList()));
292 : : }
293 [ # # # # ]: 0 : fprintf(stdout, "%s", json.toJson(QJsonDocument::Indented).constData());
294 : 0 : return 0;
295 : 0 : }
296 : : }
297 : :
298 [ # # ]: 0 : if (!p)
299 : : return 2;
300 : :
301 [ # # ]: 0 : p->execute();
302 [ # # # # : 0 : if (clp.isSet(u"verbose"_s) && !p->output().isEmpty())
# # # # #
# # # ]
303 [ # # # # : 0 : fprintf(stdout, "%s\n", qPrintable(p->output()));
# # ]
304 [ # # ]: 0 : return p->resultCode();
305 [ # # ]: 0 : } catch (const Exception &e) {
306 [ - - - - ]: 0 : fprintf(stderr, "ERROR: %s\n", qPrintable(e.errorString()));
307 : 0 : return 1;
308 : 0 : }
309 : 0 : }
|