ssukickstarter.cpp 14.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
/**
 * @file ssukickstarter.cpp
 * @copyright 2013 Jolla Ltd.
 * @author Bernd Wachter <bwachter@lart.info>
 * @date 2013
 */

#include <QStringList>
#include <QRegExp>
#include <QDirIterator>

#include "ssukickstarter.h"
13
#include "libssu/sandbox_p.h"
14
#include "libssu/ssurepomanager.h"
15
#include "libssu/ssuvariables_p.h"
16 17 18 19 20 21 22 23 24

#include "../constants.h"

/* TODO:
 * - commands from the command section should be verified
 * - allow overriding brand key
 */


25 26 27 28 29 30 31 32 33
SsuKickstarter::SsuKickstarter()
{
    SsuDeviceInfo deviceInfo;
    deviceModel = deviceInfo.deviceModel();

    if ((ssu.deviceMode() & Ssu::RndMode) == Ssu::RndMode)
        rndMode = true;
    else
        rndMode = false;
34 35
}

36 37 38 39
QStringList SsuKickstarter::commands()
{
    SsuDeviceInfo deviceInfo(deviceModel);
    QStringList result;
40

41
    QHash<QString, QString> h;
42

43
    deviceInfo.variableSection("kickstart-commands", &h);
44

45
    // read commands from variable, ...
46

47 48 49 50 51
    QHash<QString, QString>::const_iterator it = h.constBegin();
    while (it != h.constEnd()) {
        result.append(it.key() + " " + it.value());
        it++;
    }
52

53
    return result;
54 55
}

56 57 58 59 60 61 62 63 64 65 66
QStringList SsuKickstarter::commandSection(const QString &section, const QString &description)
{
    QStringList result;
    SsuDeviceInfo deviceInfo(deviceModel);
    QString commandFile;
    QFile part;

    QDir dir(Sandbox::map(QString("/%1/kickstart/%2/")
                          .arg(SSU_DATA_DIR)
                          .arg(section)));

Pekka Vuorela's avatar
Pekka Vuorela committed
67
    if (dir.exists(replaceSpaces(deviceModel.toLower()))) {
68
        commandFile = replaceSpaces(deviceModel.toLower());
Pekka Vuorela's avatar
Pekka Vuorela committed
69
    } else if (dir.exists(replaceSpaces(deviceInfo.deviceVariant(true).toLower()))) {
70
        commandFile = replaceSpaces(deviceInfo.deviceVariant(true).toLower());
Pekka Vuorela's avatar
Pekka Vuorela committed
71
    } else if (dir.exists("default")) {
72
        commandFile = "default";
Pekka Vuorela's avatar
Pekka Vuorela committed
73
    } else {
74 75 76 77 78 79
        if (description.isEmpty())
            result.append("## No suitable configuration found in " + dir.path());
        else
            result.append("## No configuration for " + description + " found.");
        return result;
    }
80

81
    QFile file(dir.path() + "/" + commandFile);
82

83 84 85 86
    if (description.isEmpty())
        result.append("### Commands from " + dir.path() + "/" + commandFile);
    else
        result.append("### " + description + " from " + commandFile);
87

88 89 90 91 92
    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QTextStream in(&file);
        while (!in.atEnd())
            result.append(in.readLine());
    }
93

94
    return result;
95 96
}

97 98 99 100
QString SsuKickstarter::replaceSpaces(const QString &value)
{
    QString retval = value;
    return retval.replace(" ", "_");
101 102
}

103 104 105 106 107 108 109 110 111 112 113 114
QStringList SsuKickstarter::repos()
{
    QStringList result;
    SsuDeviceInfo deviceInfo(deviceModel);
    SsuRepoManager repoManager;
    QTextStream qerr(stderr);

    QStringList repos = repoManager.repos(rndMode, deviceInfo, Ssu::BoardFilter);

    foreach (const QString &repo, repos) {
        QString repoUrl = ssu.repoUrl(repo, rndMode, QHash<QString, QString>(), repoOverride);

115
        if (repoUrl.isEmpty()) {
116 117 118 119 120 121 122 123 124 125 126
            qerr << "Repository " << repo << " does not have an URL, ignoring" << endl;
            continue;
        }

        // Adaptation repos need to have separate naming so that when images are done
        // the repository caches will not be mixed with each other.
        if (repo.startsWith("adaptation")) {
            result.append(QString("repo --name=%1-%2-%3%4 --baseurl=%5")
                          .arg(repo)
                          .arg(replaceSpaces(deviceModel))
                          .arg((rndMode ? repoOverride.value("rndRelease")
127
                                        : repoOverride.value("release")))
128
                          .arg((rndMode ? "-" + repoOverride.value("flavourName")
129
                                        : QString()))
130 131
                          .arg(repoUrl)
                         );
Pekka Vuorela's avatar
Pekka Vuorela committed
132
        } else {
133 134 135
            result.append(QString("repo --name=%1-%2%3 --baseurl=%4")
                          .arg(repo)
                          .arg((rndMode ? repoOverride.value("rndRelease")
136
                                        : repoOverride.value("release")))
137
                          .arg((rndMode ? "-" + repoOverride.value("flavourName")
138
                                        : QString()))
139 140
                          .arg(repoUrl)
                         );
Pekka Vuorela's avatar
Pekka Vuorela committed
141
        }
142
    }
143

144 145
    return result;
}
146

147
QStringList SsuKickstarter::packagesSection(const QString &name)
148 149
{
    QStringList result;
150

151 152 153 154 155 156
    if (name == "packages") {
        // insert @vendor configuration device
        QString configuration = QString("@%1 Configuration %2")
                                .arg(repoOverride.value("brand"))
                                .arg(deviceModel);
        result.append(configuration);
157

158 159 160 161
        result.sort();
        result.removeDuplicates();
    } else {
        result = commandSection(name);
162
    }
163

164 165 166
    result.prepend("%" + name);
    result.append("%end");
    return result;
167 168 169
}

// we intentionally don't support device-specific post scriptlets
170
QStringList SsuKickstarter::scriptletSection(const QString &name, int flags)
171 172 173 174 175 176 177 178 179
{
    QStringList result;
    QString path;
    QDir dir;

    if ((flags & NoChroot) == NoChroot)
        path = Sandbox::map(QString("/%1/kickstart/%2_nochroot/")
                            .arg(SSU_DATA_DIR)
                            .arg(name));
180
    else
181 182 183 184 185 186 187 188 189
        path = Sandbox::map(QString("/%1/kickstart/%2/")
                            .arg(SSU_DATA_DIR)
                            .arg(name));

    if ((flags & DeviceSpecific) == DeviceSpecific) {
        if (dir.exists(path + "/" + replaceSpaces(deviceModel.toLower())))
            path = path + "/" + replaceSpaces(deviceModel.toLower());
        else
            path = path + "/default";
190 191
    }

192 193 194 195 196 197 198 199 200 201 202 203 204 205
    dir.setPath(path);
    QStringList scriptlets = dir.entryList(QDir::AllEntries | QDir::NoDot | QDir::NoDotDot,
                                           QDir::Name);

    foreach (const QString &scriptlet, scriptlets) {
        QFile file(dir.filePath(scriptlet));
        result.append("### begin " + scriptlet);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            while (!in.atEnd())
                result.append(in.readLine());
        }
        result.append("### end " + scriptlet);
    }
206

207 208 209
    if (!result.isEmpty()) {
        result.prepend(QString("export SSU_RELEASE_TYPE=%1")
                       .arg(rndMode ? "rnd" : "release"));
210

211 212 213 214 215 216 217
        if ((flags & NoChroot) == NoChroot)
            result.prepend("%" + name + " --nochroot");
        else
            result.prepend("%" + name);

        result.append("%end");
    }
218

219
    return result;
220 221
}

222 223 224
void SsuKickstarter::setRepoParameters(QHash<QString, QString> parameters)
{
    repoOverride = parameters;
225

226 227
    if (repoOverride.contains("model"))
        deviceModel = repoOverride.value("model");
228 229
}

230
bool SsuKickstarter::write(const QString &kickstart)
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
{
    QTextStream qerr(stderr);
    QStringList commandSections;

    // initialize with default 'part' for compatibility, as partitions
    // used to work without configuration. It'll get replaced with
    // configuration values, if found
    commandSections.append("part");

    // rnd mode should not come from the defaults
    if (repoOverride.contains("rnd")) {
        if (repoOverride.value("rnd") == "true")
            rndMode = true;
        else if (repoOverride.value("rnd") == "false")
            rndMode = false;
246
    }
247 248 249 250

    QHash<QString, QString> defaults;
    // get generic repo variables; domain and adaptation specific bits are not interesting
    // in the kickstart
Pekka Vuorela's avatar
Pekka Vuorela committed
251
    SsuRepoManager repoManager;
252 253 254
    repoManager.repoVariables(&defaults, rndMode);

    // overwrite with kickstart defaults
Pekka Vuorela's avatar
Pekka Vuorela committed
255
    SsuDeviceInfo deviceInfo(deviceModel);
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
    deviceInfo.variableSection("kickstart-defaults", &defaults);
    if (deviceInfo.variable("kickstart-defaults", "commandSections")
            .canConvert(QMetaType::QStringList)) {
        commandSections =
            deviceInfo.variable("kickstart-defaults", "commandSections").toStringList();
    }

    QHash<QString, QString>::const_iterator it = defaults.constBegin();
    while (it != defaults.constEnd()) {
        if (!repoOverride.contains(it.key()))
            repoOverride.insert(it.key(), it.value());
        it++;
    }

    // in rnd mode both rndRelease an release should be the same,
    // as the variable name used is %(release)
    if (rndMode && repoOverride.contains("rndRelease"))
        repoOverride.insert("release", repoOverride.value("rndRelease"));

    // release mode variables should not contain flavourName
    if (!rndMode && repoOverride.contains("flavourName"))
        repoOverride.remove("flavourName");

    //TODO: check for mandatory keys, brand, ..
    if (!repoOverride.contains("deviceModel"))
        repoOverride.insert("deviceModel", deviceInfo.deviceModel());

    // do sanity checking on the model
    if (deviceInfo.contains() == false) {
        qerr << "Device model '" << deviceInfo.deviceModel() << "' does not exist" << endl;

        if (repoOverride.value("force") != "true")
            return false;
    }

    QRegExp regex(" {2,}", Qt::CaseSensitive, QRegExp::RegExp2);
    if (regex.indexIn(deviceInfo.deviceModel(), 0) != -1) {
        qerr << "Device model '" << deviceInfo.deviceModel()
             << "' contains multiple consecutive spaces." << endl;
        if (deviceInfo.contains())
            qerr << "Since the model exists it looks like your configuration is broken." << endl;
        return false;
298
    }
299 300 301 302 303 304 305 306 307 308

    if (!repoOverride.contains("brand")) {
        qerr << "No brand set. Check your configuration." << endl;
        return false;
    }

    bool opened = false;
    QString outputDir = repoOverride.value("outputdir");
    if (!outputDir.isEmpty()) outputDir.append("/");

Pekka Vuorela's avatar
Pekka Vuorela committed
309 310
    QFile ks;

311
    if (kickstart.isEmpty()) {
Pekka Vuorela's avatar
Pekka Vuorela committed
312 313
        SsuVariables var;

314 315 316 317 318 319 320 321 322 323 324 325
        if (repoOverride.contains("filename")) {
            QString fileName = QString("%1%2")
                               .arg(outputDir)
                               .arg(replaceSpaces(var.resolveString(repoOverride.value("filename"),
                                                                    &repoOverride)));

            ks.setFileName(fileName);
            opened = ks.open(QIODevice::WriteOnly);
        } else {
            qerr << "No filename specified, and no default filename configured" << endl;
            return false;
        }
Pekka Vuorela's avatar
Pekka Vuorela committed
326
    } else if (kickstart == "-") {
327
        opened = ks.open(stdout, QIODevice::WriteOnly);
Pekka Vuorela's avatar
Pekka Vuorela committed
328
    } else {
329 330 331 332 333 334 335
        ks.setFileName(outputDir + kickstart);
        opened = ks.open(QIODevice::WriteOnly);
    }

    if (!opened) {
        qerr << "Unable to write output file " << ks.fileName() << ": " << ks.errorString() << endl;
        return false;
Pekka Vuorela's avatar
Pekka Vuorela committed
336
    } else if (!ks.fileName().isEmpty()) {
337
        qerr << "Writing kickstart to " << ks.fileName() << endl;
Pekka Vuorela's avatar
Pekka Vuorela committed
338
    }
339 340 341 342 343 344 345 346 347

    QString displayName = QString("# DisplayName: %1 %2/%3 (%4) %5")
                          .arg(repoOverride.value("brand"))
                          .arg(deviceInfo.deviceModel())
                          .arg(repoOverride.value("arch"))
                          .arg((rndMode ? "rnd"
                                : "release"))
                          .arg(repoOverride.value("version"));

348
    // Feature names can be prefixed with '-' to inhibit implicit suggestion
349 350
    QStringList featuresList = deviceInfo.value("img-features").toStringList();

351
    // Add developer-mode feature to rnd images by default
352
    if (rndMode && !featuresList.contains("-developer-mode"))
353 354
        featuresList << "developer-mode";

355 356
    featuresList = featuresList.filter(QRegExp("^[^-]"));

357 358 359 360 361
    QString suggestedFeatures;

    // work around some idiotic JS list parsing on our side by terminating one-element list by comma
    if (featuresList.count() == 1)
        suggestedFeatures = QString("# SuggestedFeatures: %1,")
362
                            .arg(featuresList.join(", "));
363 364
    else if (featuresList.count() > 1)
        suggestedFeatures = QString("# SuggestedFeatures: %1")
365
                            .arg(featuresList.join(", "));
366 367 368 369 370 371 372 373 374 375 376 377

    QString imageType = "fs";
    if (!deviceInfo.value("img-type").toString().isEmpty())
        imageType = deviceInfo.value("img-type").toString();

    QString imageArchitecture = "armv7hl";
    if (!deviceInfo.value("img-arch").toString().isEmpty())
        imageArchitecture = deviceInfo.value("img-arch").toString();

    QString kickstartType = QString("# KickstartType: %1")
                            .arg((rndMode ? "rnd" : "release"));

Pekka Vuorela's avatar
Pekka Vuorela committed
378
    QTextStream kout;
379 380 381
    kout.setDevice(&ks);
    kout << displayName << endl;
    kout << kickstartType << endl;
382
    kout << "# DeviceModel: " << deviceInfo.deviceModel() << endl;
383
    kout << "# DeviceVariant: " << deviceInfo.deviceVariant(true) << endl;
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
    if (!suggestedFeatures.isEmpty())
        kout << suggestedFeatures << endl;
    kout << "# SuggestedImageType: " << imageType << endl;
    kout << "# SuggestedArchitecture: " << imageArchitecture << endl << endl;
    kout << commands().join("\n") << endl << endl;
    foreach (const QString &section, commandSections) {
        kout << commandSection(section).join("\n") << endl << endl;
    }

    // this allows simple search and replace postprocessing of the repos section
    // to overcome shortcomings of the "keep image creation simple token based"
    // approach
    // TODO: QHash looks messy in the config, provide tool to edit it
    QString repoSection = repos().join("\n");
    if (deviceInfo.variable("kickstart-defaults", "urlPostprocess")
            .canConvert(QMetaType::QVariantHash)) {
        QHash<QString, QVariant> postproc =
            deviceInfo.variable("kickstart-defaults", "urlPostprocess").toHash();

        QHash<QString, QVariant>::const_iterator it = postproc.constBegin();
        while (it != postproc.constEnd()) {
            QRegExp regex(it.key(), Qt::CaseSensitive, QRegExp::RegExp2);

            repoSection.replace(regex, it.value().toString());
            it++;
        }
    }

    kout << repoSection << endl << endl;
    kout << packagesSection("packages").join("\n") << endl << endl;
    kout << packagesSection("attachment").join("\n") << endl << endl;
    // TODO: now that extending scriptlet section is might make sense to make it configurable
    kout << scriptletSection("pre", Chroot).join("\n") << endl << endl;
    kout << scriptletSection("post", Chroot).join("\n") << endl << endl;
    kout << scriptletSection("post", NoChroot).join("\n") << endl << endl;
    kout << scriptletSection("pack", DeviceSpecific).join("\n") << endl << endl;
    // POST, die-on-error

    return true;
423
}