Skip to content
Snippets Groups Projects
maemopackagecreationstep.cpp 20.1 KiB
Newer Older
/****************************************************************************
**
** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of Qt Creator.
**
** $QT_BEGIN_LICENSE:LGPL$
** No Commercial Usage
** This file contains pre-release code and may not be distributed.
** You may use this file in accordance with the terms and conditions
** contained in the Technology Preview License Agreement accompanying
** this package.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights.  These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**
**
**
**
**
**
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "maemopackagecreationstep.h"

#include "maemoconstants.h"
#include "maemodeployables.h"
#include "maemodeploystep.h"
ck's avatar
ck committed
#include "maemoglobal.h"
#include "maemopackagecreationwidget.h"
#include "maemoprofilewrapper.h"
#include "maemotemplatesmanager.h"
ck's avatar
ck committed
#include "maemotoolchain.h"
Tobias Hunger's avatar
Tobias Hunger committed
#include <projectexplorer/buildsteplist.h>
#include <projectexplorer/projectexplorerconstants.h>
ck's avatar
ck committed
#include <qt4buildconfiguration.h>
#include <qt4project.h>
#include <qt4target.h>

#include <QtCore/QProcess>
#include <QtCore/QProcessEnvironment>
#include <QtCore/QRegExp>
ck's avatar
ck committed
#include <QtCore/QStringBuilder>
#include <QtGui/QWidget>

namespace {
    const QLatin1String PackagingEnabledKey("Packaging Enabled");
}
using namespace ProjectExplorer::Constants;
Tobias Hunger's avatar
Tobias Hunger committed
using ProjectExplorer::BuildStepList;
using ProjectExplorer::BuildStepConfigWidget;
using ProjectExplorer::Task;

namespace Qt4ProjectManager {
namespace Internal {

const QLatin1String MaemoPackageCreationStep::DefaultVersionNumber("0.0.1");

Tobias Hunger's avatar
Tobias Hunger committed
MaemoPackageCreationStep::MaemoPackageCreationStep(BuildStepList *bsl)
    : ProjectExplorer::BuildStep(bsl, CreatePackageId),
      m_packagingEnabled(true)
    ctor();
Tobias Hunger's avatar
Tobias Hunger committed
MaemoPackageCreationStep::MaemoPackageCreationStep(BuildStepList *bsl,
    MaemoPackageCreationStep *other)
Tobias Hunger's avatar
Tobias Hunger committed
    : BuildStep(bsl, other),
      m_packagingEnabled(other->m_packagingEnabled)
    ctor();
MaemoPackageCreationStep::~MaemoPackageCreationStep()
{
}
void MaemoPackageCreationStep::ctor()
{
    setDefaultDisplayName(tr("Packaging for Maemo"));

    m_lastBuildConfig = qt4BuildConfiguration();
    connect(target(),
        SIGNAL(activeBuildConfigurationChanged(ProjectExplorer::BuildConfiguration*)),
        this, SLOT(handleBuildConfigChanged()));
    handleBuildConfigChanged();
bool MaemoPackageCreationStep::init()
{
    return true;
}

ck's avatar
ck committed
QVariantMap MaemoPackageCreationStep::toMap() const
{
    QVariantMap map(ProjectExplorer::BuildStep::toMap());
ck's avatar
ck committed
    map.insert(PackagingEnabledKey, m_packagingEnabled);
ck's avatar
ck committed
}

bool MaemoPackageCreationStep::fromMap(const QVariantMap &map)
{
ck's avatar
ck committed
    m_packagingEnabled = map.value(PackagingEnabledKey, true).toBool();
ck's avatar
ck committed
    return ProjectExplorer::BuildStep::fromMap(map);
}

void MaemoPackageCreationStep::run(QFutureInterface<bool> &fi)
{
    bool success;
    if (m_packagingEnabled) {
        QProcess * const buildProc = new QProcess;
        connect(buildProc, SIGNAL(readyReadStandardOutput()), this,
            SLOT(handleBuildOutput()));
        connect(buildProc, SIGNAL(readyReadStandardError()), this,
            SLOT(handleBuildOutput()));
        success = createPackage(buildProc);
        disconnect(buildProc, 0, this, 0);
        buildProc->deleteLater();
    } else  {
        success = true;
    }
    fi.reportResult(success);
}

BuildStepConfigWidget *MaemoPackageCreationStep::createConfigWidget()
{
    return new MaemoPackageCreationWidget(this);
}

bool MaemoPackageCreationStep::createPackage(QProcess *buildProc)
ck's avatar
ck committed
{
    if (!packagingNeeded()) {
        emit addOutput(tr("Package up to date."), MessageOutput);
ck's avatar
ck committed
        return true;
    emit addOutput(tr("Creating package file ..."), MessageOutput);
    if (!preparePackagingProcess(buildProc, maemoToolChain(), buildDirectory(),
        &error)) {
        raiseError(error);
ck's avatar
ck committed
        return false;
    }
    const QString projectDir
        = buildConfiguration()->target()->project()->projectDirectory();
    const bool inSourceBuild
        = QFileInfo(buildDirectory()) == QFileInfo(projectDir);
    if (!inSourceBuild && !copyDebianFiles())
    if (!runCommand(buildProc, QLatin1String("dpkg-buildpackage -nc -uc -us")))
ck's avatar
ck committed
        return false;
    // Workaround for non-working dh_builddeb --destdir=.
    if (!QDir(buildDirectory()).isRoot()) {
        const ProjectExplorer::Project * const project
            = buildConfiguration()->target()->project();
        QString error;
        const QString pkgFileName = packageFileName(project,
            MaemoTemplatesManager::instance()->version(project, &error));
        if (!error.isEmpty())
            raiseError(tr("Packaging failed."), error);
        const QString changesFileName = QFileInfo(pkgFileName)
            .completeBaseName() + QLatin1String(".changes");
        const QString packageSourceDir = buildDirectory() + QLatin1String("/../");
        const QString packageSourceFilePath
            = packageSourceDir + pkgFileName;
        const QString changesSourceFilePath
            = packageSourceDir + changesFileName;
        const QString changesTargetFilePath
            = buildDirectory() + QLatin1Char('/') + changesFileName;
        QFile::remove(packageFilePath());
        QFile::remove(changesTargetFilePath);
        if (!QFile::rename(packageSourceFilePath, packageFilePath())
            || !QFile::rename(changesSourceFilePath, changesTargetFilePath)) {
            raiseError(tr("Packaging failed."),
                tr("Could not move package files from %1 to %2.")
                .arg(packageSourceDir, buildDirectory()));
            return false;
        }
    emit addOutput(tr("Package created."), BuildStep::MessageOutput);
    deployStep()->deployables()->setUnmodified();
        buildProc->start(packagingCommand(maemoToolChain(),
            QLatin1String("dh_clean")));
        buildProc->waitForFinished();
    }
    return true;
}

bool MaemoPackageCreationStep::copyDebianFiles()
{
    const QString debianDirPath = buildDirectory() + QLatin1String("/debian");
    if (!removeDirectory(debianDirPath)) {
        raiseError(tr("Packaging failed."),
            tr("Could not remove directory '%1'.").arg(debianDirPath));
        return false;
    }
    QDir buildDir(buildDirectory());
    if (!buildDir.mkdir("debian")) {
        raiseError(tr("Could not create Debian directory '%1'.")
                   .arg(debianDirPath));
        return false;
    }
    const QString templatesDirPath = MaemoTemplatesManager::instance()
        ->debianDirPath(buildConfiguration()->target()->project());
    QDir templatesDir(templatesDirPath);
    const QStringList &files = templatesDir.entryList(QDir::Files);
    const bool harmattanWorkaroundNeeded
        = maemoToolChain()->version() == MaemoToolChain::Maemo6
            && !qt4BuildConfiguration()->qt4Target()->qt4Project()
                   ->applicationProFiles().isEmpty();
    foreach (const QString &fileName, files) {
        const QString srcFile
                = templatesDirPath + QLatin1Char('/') + fileName;
        const QString destFile
                = debianDirPath + QLatin1Char('/') + fileName;
        if (!QFile::copy(srcFile, destFile)) {
            raiseError(tr("Could not copy file '%1' to '%2'")
                       .arg(QDir::toNativeSeparators(srcFile),
                            QDir::toNativeSeparators(destFile)));
            return false;
        }

        // Workaround for Harmattan icon bug
        if (harmattanWorkaroundNeeded && fileName == QLatin1String("rules"))
            addWorkaroundForHarmattanBug(destFile);
ck's avatar
ck committed
    return true;
}

bool MaemoPackageCreationStep::removeDirectory(const QString &dirPath)
{
    QDir dir(dirPath);
    if (!dir.exists())
        return true;

    const QStringList &files
        = dir.entryList(QDir::Files | QDir::Hidden | QDir::System);
    foreach (const QString &fileName, files) {
        if (!dir.remove(fileName))
            return false;
    }

    const QStringList &subDirs
        = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
    foreach (const QString &subDirName, subDirs) {
        if (!removeDirectory(dirPath + QLatin1Char('/') + subDirName))
            return false;
    }

    return dir.rmdir(dirPath);
}

bool MaemoPackageCreationStep::runCommand(QProcess *buildProc,
    const QString &command)
ck's avatar
ck committed
{
    emit addOutput(tr("Package Creation: Running command '%1'.").arg(command), BuildStep::MessageOutput);
    buildProc->start(packagingCommand(maemoToolChain(), command));
    if (!buildProc->waitForStarted()) {
        raiseError(tr("Packaging failed."),
            tr("Packaging error: Could not start command '%1'. Reason: %2")
            .arg(command).arg(buildProc->errorString()));
    buildProc->waitForFinished(-1);
    if (buildProc->error() != QProcess::UnknownError
        || buildProc->exitCode() != 0) {
        QString mainMessage = tr("Packaging Error: Command '%1' failed.")
            .arg(command);
        if (buildProc->error() != QProcess::UnknownError)
            mainMessage += tr(" Reason: %1").arg(buildProc->errorString());
            mainMessage += tr("Exit code: %1").arg(buildProc->exitCode());
        raiseError(mainMessage);
ck's avatar
ck committed
        return false;
    }
    return true;
}

void MaemoPackageCreationStep::handleBuildOutput()
{
    QProcess * const buildProc = qobject_cast<QProcess *>(sender());
    if (!buildProc)
        return;
    const QByteArray &stdOut = buildProc->readAllStandardOutput();
    if (!stdOut.isEmpty())
        emit addOutput(QString::fromLocal8Bit(stdOut), BuildStep::NormalOutput);
    const QByteArray &errorOut = buildProc->readAllStandardError();
    if (!errorOut.isEmpty()) {
        emit addOutput(QString::fromLocal8Bit(errorOut), BuildStep::ErrorOutput);
void MaemoPackageCreationStep::handleBuildConfigChanged()
{
    if (m_lastBuildConfig)
        disconnect(m_lastBuildConfig, 0, this, 0);
    m_lastBuildConfig = qt4BuildConfiguration();
    connect(m_lastBuildConfig, SIGNAL(qtVersionChanged()), this,
        SIGNAL(qtVersionChanged()));
    connect(m_lastBuildConfig, SIGNAL(buildDirectoryChanged()), this,
        SIGNAL(packageFilePathChanged()));
    emit qtVersionChanged();
    emit packageFilePathChanged();
}

ck's avatar
ck committed
const Qt4BuildConfiguration *MaemoPackageCreationStep::qt4BuildConfiguration() const
{
    return static_cast<Qt4BuildConfiguration *>(buildConfiguration());
}

QString MaemoPackageCreationStep::buildDirectory() const
{
    return qt4BuildConfiguration()->buildDirectory();
QString MaemoPackageCreationStep::projectName() const
ck's avatar
ck committed
{
    return qt4BuildConfiguration()->qt4Target()->qt4Project()
        ->rootProjectNode()->displayName().toLower();
ck's avatar
ck committed
}

const MaemoToolChain *MaemoPackageCreationStep::maemoToolChain() const
{
    return static_cast<MaemoToolChain *>(qt4BuildConfiguration()->toolChain());
}

MaemoDeployStep *MaemoPackageCreationStep::deployStep() const
{
    MaemoDeployStep * const deployStep
        = MaemoGlobal::buildStep<MaemoDeployStep>(target()->activeDeployConfiguration());
    Q_ASSERT(deployStep &&
        "Fatal error: Maemo build configuration without deploy step.");
    return deployStep;
}

ck's avatar
ck committed
QString MaemoPackageCreationStep::maddeRoot() const
{
    return maemoToolChain()->maddeRoot();
}

QString MaemoPackageCreationStep::targetRoot() const
{
    return maemoToolChain()->targetRoot();
}

bool MaemoPackageCreationStep::packagingNeeded() const
{
    const MaemoDeployables * const deployables = deployStep()->deployables();
    QFileInfo packageInfo(packageFilePath());
    if (!packageInfo.exists() || deployables->isModified())
    const int deployableCount = deployables->deployableCount();
    for (int i = 0; i < deployableCount; ++i) {
        if (packageInfo.lastModified()
            <= QFileInfo(deployables->deployableAt(i).localFilePath)
               .lastModified())
            return true;
    }

    const ProjectExplorer::Project * const project = target()->project();
    const MaemoTemplatesManager * const templatesManager
        = MaemoTemplatesManager::instance();
    const QString debianPath = templatesManager->debianDirPath(project);
    if (packageInfo.lastModified() <= QFileInfo(debianPath).lastModified())
        return true;
    const QStringList debianFiles = templatesManager->debianFiles(project);
    foreach (const QString &debianFile, debianFiles) {
        const QString absFilePath = debianPath + QLatin1Char('/') + debianFile;
        if (packageInfo.lastModified() <= QFileInfo(absFilePath).lastModified())
            return true;
    }

    return false;
QString MaemoPackageCreationStep::packageFilePath() const
{
    QString error;
    const QString &version = versionString(&error);
    if (version.isEmpty())
        return QString();
    return buildDirectory() % '/'
        % packageFileName(buildConfiguration()->target()->project(), version);
bool MaemoPackageCreationStep::isPackagingEnabled() const
{
    return m_packagingEnabled || !maemoToolChain()->allowsPackagingDisabling();
}

QString MaemoPackageCreationStep::versionString(QString *error) const
    return MaemoTemplatesManager::instance()
        ->version(buildConfiguration()->target()->project(), error);

bool MaemoPackageCreationStep::setVersionString(const QString &version,
    QString *error)
    const bool success = MaemoTemplatesManager::instance()
        ->setVersion(buildConfiguration()->target()->project(), version, error);
    if (success)
        emit packageFilePathChanged();
    return success;
QString MaemoPackageCreationStep::nativePath(const QFile &file)
{
    return QDir::toNativeSeparators(QFileInfo(file).filePath());
}

void MaemoPackageCreationStep::raiseError(const QString &shortMsg,
                                          const QString &detailedMsg)
{
    emit addOutput(detailedMsg.isNull() ? shortMsg : detailedMsg, BuildStep::ErrorOutput);
    emit addTask(Task(Task::Error, shortMsg, QString(), -1,
                      TASK_CATEGORY_BUILDSYSTEM));
}

bool MaemoPackageCreationStep::preparePackagingProcess(QProcess *proc,
    const MaemoToolChain *tc, const QString &workingDir, QString *error)
{
    QFile configFile(tc->targetRoot() % QLatin1String("/config.sh"));
    if (!configFile.open(QIODevice::ReadOnly)) {
        *error = tr("Cannot open MADDE config file '%1'.")
            .arg(nativePath(configFile));
        return false;
    }

    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
    const QString &path
        = QDir::toNativeSeparators(tc->maddeRoot() + QLatin1Char('/'));

    const QLatin1String key("PATH");
    QString colon = QLatin1String(":");
#ifdef Q_OS_WIN
    colon = QLatin1String(";");
    env.insert(key, path % QLatin1String("bin") % colon % env.value(key));
#endif

    env.insert(key, tc->targetRoot() % "/bin" % colon % env.value(key));
    env.insert(key, path % QLatin1String("madbin") % colon % env.value(key));

    QString perlLib
        = QDir::fromNativeSeparators(path % QLatin1String("madlib/perl5"));
#ifdef Q_OS_WIN
    perlLib = perlLib.remove(QLatin1Char(':'));
    perlLib = perlLib.prepend(QLatin1Char('/'));
#endif
    env.insert(QLatin1String("PERL5LIB"), perlLib);
    env.insert(QLatin1String("PWD"), workingDir);

    const QRegExp envPattern(QLatin1String("([^=]+)=[\"']?([^;\"']+)[\"']? ;.*"));
    QByteArray line;
    do {
        line = configFile.readLine(200);
        if (envPattern.exactMatch(line))
            env.insert(envPattern.cap(1), envPattern.cap(2));
    } while (!line.isEmpty());

    proc->setProcessEnvironment(env);
    proc->setWorkingDirectory(workingDir);
    proc->start("cd " + workingDir);
    proc->waitForFinished();
    return true;
}

QString MaemoPackageCreationStep::packagingCommand(const MaemoToolChain *tc,
    const QString &commandName)
{
    QString perl;
#ifdef Q_OS_WIN
    perl = tc->maddeRoot() + QLatin1String("/bin/perl.exe ");
#endif
    return perl + tc->maddeRoot() % QLatin1String("/madbin/") % commandName;
}

void MaemoPackageCreationStep::checkProjectName()
{
    const QRegExp legalName(QLatin1String("[0-9-+a-z\\.]+"));
    if (!legalName.exactMatch(buildConfiguration()->target()->project()->displayName())) {
        emit addTask(Task(Task::Warning,
            tr("Your project name contains characters not allowed in Debian packages.\n"
               "They must only use lower-case letters, numbers, '-', '+' and '.'.\n"
               "We will try to work around that, but you may experience problems."),
               QString(), -1, TASK_CATEGORY_BUILDSYSTEM));
    }
}

QString MaemoPackageCreationStep::packageName(const ProjectExplorer::Project *project)
{
    QString packageName = project->displayName().toLower();
    const QRegExp legalLetter(QLatin1String("[a-z0-9+-.]"), Qt::CaseSensitive,
        QRegExp::WildcardUnix);
    for (int i = 0; i < packageName.length(); ++i) {
        if (!legalLetter.exactMatch(packageName.mid(i, 1)))
            packageName[i] = QLatin1Char('-');
    }
    return packageName;
}

QString MaemoPackageCreationStep::packageFileName(const ProjectExplorer::Project *project,
    const QString &version)
{
    return packageName(project) % QLatin1Char('_') % version
        % QLatin1String("_armel.deb");
}

void MaemoPackageCreationStep::addWorkaroundForHarmattanBug(const QString &rulesFilePath)
{
    QFile rulesFile(rulesFilePath);
    if (!rulesFile.open(QIODevice::ReadWrite)) {
        qWarning("Cannot open rules file for Maemo6 icon path adaptation.");
        return;
    }
    QByteArray content = rulesFile.readAll();
    const int makeInstallLine = content.indexOf("\t$(MAKE) INSTALL_ROOT");
    if (makeInstallLine == -1)
        return;
    const int makeInstallEol = content.indexOf('\n', makeInstallLine);
    if (makeInstallEol == -1)
        return;
    const QByteArray lineBefore("Icon=" + projectName().toUtf8());
    const QByteArray lineAfter("Icon=/usr/share/icons/hicolor/64x64/apps/"
        + projectName().toUtf8() + ".png");
    QString desktopFileDir = QFileInfo(rulesFile).dir().path()
        + QLatin1Char('/') + projectName()
        + QLatin1String("/usr/share/applications/");
    desktopFileDir.remove(QLatin1Char(':'));
    desktopFileDir.prepend(QLatin1Char('/'));
    const QList<Qt4ProFileNode *> &proFiles = qt4BuildConfiguration()
        ->qt4Target()->qt4Project()->applicationProFiles();
    int insertPos = makeInstallEol + 1;
    foreach (const Qt4ProFileNode * const proFile, proFiles) {
        const QString appName = proFile->targetInformation().target;
        const QString desktopFilePath
            = desktopFileDir +  appName + QLatin1String(".desktop");
        const QString tmpFile
            = desktopFileDir + appName + QLatin1String(".sed");
        const QByteArray sedCmd = "\tsed 's:" + lineBefore + ':' + lineAfter
            + ":' " + desktopFilePath.toLocal8Bit() + " > " + tmpFile.toUtf8()
            + '\n';
        const QByteArray mvCmd = "\tmv " + tmpFile.toUtf8() + ' '
            + desktopFilePath.toUtf8() + '\n';
        content.insert(insertPos, sedCmd);
        insertPos += sedCmd.length();
        content.insert(insertPos, mvCmd);
        insertPos += mvCmd.length();
    }
    rulesFile.resize(0);
    rulesFile.write(content);
}

const QLatin1String MaemoPackageCreationStep::CreatePackageId("Qt4ProjectManager.MaemoPackageCreationStep");

} // namespace Internal
} // namespace Qt4ProjectManager