1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (info@qt.nokia.com)
10 ** GNU Lesser General Public License Usage
12 ** This file may be used under the terms of the GNU Lesser General Public
13 ** License version 2.1 as published by the Free Software Foundation and
14 ** appearing in the file LICENSE.LGPL included in the packaging of this file.
15 ** Please review the following information to ensure the GNU Lesser General
16 ** Public License version 2.1 requirements will be met:
17 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
19 ** In addition, as a special exception, Nokia gives you certain additional
20 ** rights. These rights are described in the Nokia Qt LGPL Exception
21 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
25 ** Alternatively, this file may be used in accordance with the terms and
26 ** conditions contained in a signed written agreement between you and Nokia.
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
31 **************************************************************************/
32 #include "maemopublisherfremantlefree.h"
34 #include "maemodeployablelistmodel.h"
35 #include "maemodeploystep.h"
36 #include "maemoglobal.h"
37 #include "maemopackagecreationstep.h"
38 #include "maemopublishingfileselectiondialog.h"
39 #include "qt4maemotarget.h"
41 #include <coreplugin/ifile.h>
42 #include <projectexplorer/project.h>
43 #include <projectexplorer/target.h>
44 #include <qt4projectmanager/qmakestep.h>
45 #include <qt4projectmanager/qt4buildconfiguration.h>
47 #include <QtCore/QCoreApplication>
48 #include <QtCore/QDir>
49 #include <QtCore/QFileInfo>
50 #include <QtCore/QStringList>
51 #include <QtGui/QIcon>
53 #define ASSERT_STATE(state) ASSERT_STATE_GENERIC(State, state, m_state)
56 using namespace Utils;
58 namespace Qt4ProjectManager {
61 MaemoPublisherFremantleFree::MaemoPublisherFremantleFree(const ProjectExplorer::Project *project,
66 m_sshParams(SshConnectionParameters::DefaultProxy)
68 m_sshParams.authenticationType = SshConnectionParameters::AuthenticationByKey;
69 m_sshParams.timeout = 30;
70 m_sshParams.port = 22;
71 m_process = new QProcess(this);
72 connect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)),
73 SLOT(handleProcessFinished()));
74 connect(m_process, SIGNAL(error(QProcess::ProcessError)),
75 SLOT(handleProcessError(QProcess::ProcessError)));
76 connect(m_process, SIGNAL(readyReadStandardOutput()),
77 SLOT(handleProcessStdOut()));
78 connect(m_process, SIGNAL(readyReadStandardError()),
79 SLOT(handleProcessStdErr()));
82 MaemoPublisherFremantleFree::~MaemoPublisherFremantleFree()
84 ASSERT_STATE(Inactive);
88 void MaemoPublisherFremantleFree::publish()
93 void MaemoPublisherFremantleFree::setSshParams(const QString &hostName,
94 const QString &userName, const QString &keyFile, const QString &remoteDir)
97 m_sshParams.host = hostName;
98 m_sshParams.userName = userName;
99 m_sshParams.privateKeyFile = keyFile;
100 m_remoteDir = remoteDir;
103 void MaemoPublisherFremantleFree::cancel()
105 finishWithFailure(tr("Canceled."), tr("Publishing canceled by user."));
108 void MaemoPublisherFremantleFree::createPackage()
110 setState(CopyingProjectDir);
112 const QStringList &problems = findProblems();
113 if (!problems.isEmpty()) {
114 const QLatin1String separator("\n- ");
115 finishWithFailure(tr("The project is missing some information "
116 "important to publishing:") + separator + problems.join(separator),
117 tr("Publishing failed: Missing project information."));
121 m_tmpProjectDir = tmpDirContainer() + QLatin1Char('/')
122 + m_project->displayName();
123 if (QFileInfo(tmpDirContainer()).exists()) {
124 emit progressReport(tr("Removing left-over temporary directory ..."));
126 if (!MaemoGlobal::removeRecursively(tmpDirContainer(), error)) {
127 finishWithFailure(tr("Error removing temporary directory: %1").arg(error),
128 tr("Publishing failed: Could not create source package."));
133 emit progressReport(tr("Setting up temporary directory ..."));
134 if (!QDir::temp().mkdir(QFileInfo(tmpDirContainer()).fileName())) {
135 finishWithFailure(tr("Error: Could not create temporary directory."),
136 tr("Publishing failed: Could not create source package."));
139 if (!copyRecursively(m_project->projectDirectory(), m_tmpProjectDir)) {
140 finishWithFailure(tr("Error: Could not copy project directory"),
141 tr("Publishing failed: Could not create source package."));
144 if (!fixNewlines()) {
145 finishWithFailure(tr("Error: Could not fix newlines"),
146 tr("Publishing failed: Could not create source package."));
151 if (!updateDesktopFiles(&error)) {
152 finishWithFailure(error,
153 tr("Publishing failed: Could not create package."));
157 emit progressReport(tr("Cleaning up temporary directory ..."));
158 MaemoPackageCreationStep::preparePackagingProcess(m_process,
159 m_buildConfig, m_tmpProjectDir);
160 setState(RunningQmake);
161 ProjectExplorer::AbstractProcessStep * const qmakeStep
162 = m_buildConfig->qmakeStep();
164 const ProjectExplorer::ProcessParameters * const pp
165 = qmakeStep->processParameters();
166 m_process->start(pp->effectiveCommand() + QLatin1String(" ")
167 + pp->effectiveArguments());
170 bool MaemoPublisherFremantleFree::copyRecursively(const QString &srcFilePath,
171 const QString &tgtFilePath)
173 if (m_state == Inactive)
176 QFileInfo srcFileInfo(srcFilePath);
177 if (srcFileInfo.isDir()) {
178 if (srcFileInfo == QFileInfo(m_project->projectDirectory()
179 + QLatin1String("/debian")))
181 QString actualSourcePath = srcFilePath;
182 QString actualTargetPath = tgtFilePath;
184 if (srcFileInfo.fileName() == QLatin1String("qtc_packaging")) {
185 actualSourcePath += QLatin1String("/debian_fremantle");
186 actualTargetPath.replace(QRegExp(QLatin1String("qtc_packaging$")),
187 QLatin1String("debian"));
190 QDir targetDir(actualTargetPath);
192 if (!targetDir.mkdir(QFileInfo(actualTargetPath).fileName())) {
193 emit progressReport(tr("Failed to create directory '%1'.")
194 .arg(QDir::toNativeSeparators(actualTargetPath)), ErrorOutput);
197 QDir sourceDir(actualSourcePath);
198 QStringList fileNames = sourceDir.entryList(QDir::Files | QDir::Hidden
199 | QDir::System | QDir::Dirs | QDir::NoDotAndDotDot);
200 foreach (const QString &fileName, fileNames) {
201 if (!copyRecursively(actualSourcePath + QLatin1Char('/') + fileName,
202 actualTargetPath + QLatin1Char('/') + fileName))
206 if (!QFile::copy(srcFilePath, tgtFilePath)) {
207 emit progressReport(tr("Could not copy file '%1' to '%2'.")
208 .arg(QDir::toNativeSeparators(srcFilePath),
209 QDir::toNativeSeparators(tgtFilePath)));
212 QCoreApplication::processEvents();
214 if (tgtFilePath == m_tmpProjectDir + QLatin1String("/debian/rules")) {
215 QFile rulesFile(tgtFilePath);
216 if (!rulesFile.open(QIODevice::ReadWrite)) {
217 emit progressReport(tr("Error: Cannot open file '%1'.")
218 .arg(QDir::toNativeSeparators(tgtFilePath)));
221 QByteArray rulesContents = rulesFile.readAll();
222 rulesContents.replace("$(MAKE) clean", "# $(MAKE) clean");
223 rulesContents.replace("# Add here commands to configure the package.",
224 "qmake " + QFileInfo(m_project->file()->fileName()).fileName().toLocal8Bit());
225 MaemoPackageCreationStep::ensureShlibdeps(rulesContents);
227 rulesFile.write(rulesContents);
233 bool MaemoPublisherFremantleFree::fixNewlines()
235 QDir debianDir(m_tmpProjectDir + QLatin1String("/debian"));
236 const QStringList &fileNames = debianDir.entryList(QDir::Files);
237 foreach (const QString &fileName, fileNames) {
238 QFile file(debianDir.filePath(fileName));
239 if (!file.open(QIODevice::ReadWrite))
241 QByteArray contents = file.readAll();
242 const QByteArray crlf("\r\n");
243 if (!contents.contains(crlf))
245 contents.replace(crlf, "\n");
247 file.write(contents);
252 void MaemoPublisherFremantleFree::handleProcessError(QProcess::ProcessError error)
254 if (error == QProcess::FailedToStart)
255 handleProcessFinished(true);
258 void MaemoPublisherFremantleFree::handleProcessFinished()
260 handleProcessFinished(false);
263 void MaemoPublisherFremantleFree::handleProcessStdOut()
265 if (m_state == RunningQmake || m_state == RunningMakeDistclean
266 || m_state == BuildingPackage) {
267 emit progressReport(QString::fromLocal8Bit(m_process->readAllStandardOutput()),
272 void MaemoPublisherFremantleFree::handleProcessStdErr()
274 if (m_state == RunningQmake || m_state == RunningMakeDistclean
275 || m_state == BuildingPackage) {
276 emit progressReport(QString::fromLocal8Bit(m_process->readAllStandardError()),
281 void MaemoPublisherFremantleFree::handleProcessFinished(bool failedToStart)
283 ASSERT_STATE(QList<State>() << RunningQmake << RunningMakeDistclean
284 << BuildingPackage << Inactive);
288 if (failedToStart || m_process->exitStatus() != QProcess::NormalExit
289 ||m_process->exitCode() != 0) {
290 runDpkgBuildPackage();
292 setState(RunningMakeDistclean);
293 m_process->start(m_buildConfig->makeCommand(),
294 QStringList() << QLatin1String("distclean"));
297 case RunningMakeDistclean:
298 runDpkgBuildPackage();
300 case BuildingPackage: {
303 error = tr("Error: Failed to start dpkg-buildpackage.");
304 } else if (m_process->exitStatus() != QProcess::NormalExit
305 || m_process->exitCode() != 0) {
306 error = tr("Error: dpkg-buildpackage did not succeed.");
309 if (!error.isEmpty()) {
310 finishWithFailure(error, tr("Package creation failed."));
314 QDir dir(tmpDirContainer());
315 const QStringList &fileNames = dir.entryList(QDir::Files);
316 foreach (const QString &fileName, fileNames) {
317 const QString filePath
318 = tmpDirContainer() + QLatin1Char('/') + fileName;
319 if (fileName.endsWith(QLatin1String(".dsc")))
320 m_filesToUpload.append(filePath);
322 m_filesToUpload.prepend(filePath);
325 emit progressReport(tr("Done."));
326 QStringList nativeFilePaths;
327 foreach (const QString &filePath, m_filesToUpload)
328 nativeFilePaths << QDir::toNativeSeparators(filePath);
329 m_resultString = tr("Packaging finished successfully. "
330 "The following files were created:\n")
331 + nativeFilePaths.join(QLatin1String("\n"));
343 void MaemoPublisherFremantleFree::runDpkgBuildPackage()
345 MaemoPublishingFileSelectionDialog d(m_tmpProjectDir);
346 if (d.exec() == QDialog::Rejected) {
350 foreach (const QString &filePath, d.filesToExclude()) {
352 if (!MaemoGlobal::removeRecursively(filePath, error)) {
353 finishWithFailure(error,
354 tr("Publishing failed: Could not create package."));
358 if (m_state == Inactive)
360 setState(BuildingPackage);
361 emit progressReport(tr("Building source package..."));
362 const QStringList args = QStringList() << QLatin1String("dpkg-buildpackage")
363 << QLatin1String("-S") << QLatin1String("-us") << QLatin1String("-uc");
364 MaemoGlobal::callMad(*m_process, args, m_buildConfig->qtVersion(), true);
367 // We have to implement the SCP protocol, because the maemo.org
368 // webmaster refuses to enable SFTP "for security reasons" ...
369 void MaemoPublisherFremantleFree::uploadPackage()
371 m_uploader = SshRemoteProcessRunner::create(m_sshParams);
372 connect(m_uploader.data(), SIGNAL(processStarted()),
373 SLOT(handleScpStarted()));
374 connect(m_uploader.data(), SIGNAL(connectionError(Utils::SshError)),
375 SLOT(handleConnectionError()));
376 connect(m_uploader.data(), SIGNAL(processClosed(int)),
377 SLOT(handleUploadJobFinished(int)));
378 connect(m_uploader.data(), SIGNAL(processOutputAvailable(QByteArray)),
379 SLOT(handleScpStdOut(QByteArray)));
380 emit progressReport(tr("Starting scp ..."));
381 setState(StartingScp);
382 m_uploader->run("scp -td " + m_remoteDir.toUtf8());
385 void MaemoPublisherFremantleFree::handleScpStarted()
387 ASSERT_STATE(QList<State>() << StartingScp << Inactive);
389 if (m_state == StartingScp)
393 void MaemoPublisherFremantleFree::handleConnectionError()
395 if (m_state != Inactive) {
396 finishWithFailure(tr("SSH error: %1").arg(m_uploader->connection()->errorString()),
397 tr("Upload failed."));
401 void MaemoPublisherFremantleFree::handleUploadJobFinished(int exitStatus)
403 ASSERT_STATE(QList<State>() << PreparingToUploadFile << UploadingFile
406 if (m_state != Inactive && (exitStatus != SshRemoteProcess::ExitedNormally
407 || m_uploader->process()->exitCode() != 0)) {
409 if (exitStatus != SshRemoteProcess::ExitedNormally) {
410 error = tr("Error uploading file: %1")
411 .arg(m_uploader->process()->errorString());
413 error = tr("Error uploading file.");
415 finishWithFailure(error, tr("Upload failed."));
419 void MaemoPublisherFremantleFree::prepareToSendFile()
421 if (m_filesToUpload.isEmpty()) {
422 emit progressReport(tr("All files uploaded."));
423 m_resultString = tr("Upload succeeded. You should shortly "
424 "receive an email informing you about the outcome "
425 "of the build process.");
430 setState(PreparingToUploadFile);
431 const QString &nextFilePath = m_filesToUpload.first();
432 emit progressReport(tr("Uploading file %1 ...")
433 .arg(QDir::toNativeSeparators(nextFilePath)));
434 QFileInfo info(nextFilePath);
435 m_uploader->process()->sendInput("C0644 " + QByteArray::number(info.size())
436 + ' ' + info.fileName().toUtf8() + '\n');
439 void MaemoPublisherFremantleFree::sendFile()
441 Q_ASSERT(!m_filesToUpload.isEmpty());
442 Q_ASSERT(m_state == PreparingToUploadFile);
444 setState(UploadingFile);
445 const QString filePath = m_filesToUpload.takeFirst();
446 QFile file(filePath);
447 if (!file.open(QIODevice::ReadOnly)) {
448 finishWithFailure(tr("Cannot open file for reading: %1")
449 .arg(file.errorString()), tr("Upload failed."));
452 qint64 bytesToSend = file.size();
453 while (bytesToSend > 0) {
454 const QByteArray &data
455 = file.read(qMin(bytesToSend, Q_INT64_C(1024*1024)));
456 if (data.count() == 0) {
457 finishWithFailure(tr("Cannot read file: %1").arg(file.errorString()),
458 tr("Upload failed."));
461 m_uploader->process()->sendInput(data);
462 bytesToSend -= data.size();
463 QCoreApplication::processEvents();
464 if (m_state == Inactive)
467 m_uploader->process()->sendInput(QByteArray(1, '\0'));
470 void MaemoPublisherFremantleFree::handleScpStdOut(const QByteArray &output)
472 ASSERT_STATE(QList<State>() << PreparingToUploadFile << UploadingFile
475 if (m_state == Inactive)
478 m_scpOutput += output;
479 if (m_scpOutput == QByteArray(1, '\0')) {
482 case PreparingToUploadFile:
491 } else if (m_scpOutput.endsWith('\n')) {
492 const QByteArray error = m_scpOutput.mid(1, m_scpOutput.count() - 2);
493 QString progressError;
494 if (!error.isEmpty()) {
495 progressError = tr("Error uploading file: %1")
496 .arg(QString::fromUtf8(error));
498 progressError = tr("Error uploading file.");
500 finishWithFailure(progressError, tr("Upload failed."));
504 QString MaemoPublisherFremantleFree::tmpDirContainer() const
506 return QDir::tempPath() + QLatin1String("/qtc_packaging_")
507 + m_project->displayName();
510 void MaemoPublisherFremantleFree::finishWithFailure(const QString &progressMsg,
511 const QString &resultMsg)
513 if (!progressMsg.isEmpty())
514 emit progressReport(progressMsg, ErrorOutput);
515 m_resultString = resultMsg;
519 bool MaemoPublisherFremantleFree::updateDesktopFiles(QString *error) const
522 MaemoDeployStep * const deployStep
523 = MaemoGlobal::buildStep<MaemoDeployStep>(m_buildConfig->target()
524 ->activeDeployConfiguration());
525 for (int i = 0; i < deployStep->deployables()->modelCount(); ++i) {
526 const MaemoDeployableListModel * const model
527 = deployStep->deployables()->modelAt(i);
528 QString desktopFilePath = model->localDesktopFilePath();
529 if (desktopFilePath.isEmpty())
531 desktopFilePath.replace(model->projectDir(), m_tmpProjectDir);
532 QFile desktopFile(desktopFilePath);
533 const QString executableFilePath = model->remoteExecutableFilePath();
534 if (executableFilePath.isEmpty()) {
535 qDebug("%s: Skipping subproject %s with missing deployment information.",
536 Q_FUNC_INFO, qPrintable(model->proFilePath()));
539 if (!desktopFile.exists() || !desktopFile.open(QIODevice::ReadWrite)) {
542 *error = tr("Failed to adapt desktop file '%1'.")
543 .arg(desktopFilePath);
547 QByteArray desktopFileContents = desktopFile.readAll();
548 bool fileNeedsUpdate = addOrReplaceDesktopFileValue(desktopFileContents,
549 "Exec", executableFilePath.toUtf8());
550 if (fileNeedsUpdate) {
551 desktopFile.resize(0);
552 desktopFile.write(desktopFileContents);
558 bool MaemoPublisherFremantleFree::addOrReplaceDesktopFileValue(QByteArray &fileContent,
559 const QByteArray &key, const QByteArray &newValue) const
561 const int keyPos = fileContent.indexOf(key + '=');
563 if (!fileContent.endsWith('\n'))
565 fileContent += key + '=' + newValue + '\n';
568 int nextNewlinePos = fileContent.indexOf('\n', keyPos);
569 if (nextNewlinePos == -1)
570 nextNewlinePos = fileContent.count();
571 const int replacePos = keyPos + key.count() + 1;
572 const int replaceCount = nextNewlinePos - replacePos;
573 const QByteArray &oldValue = fileContent.mid(replacePos, replaceCount);
574 if (oldValue == newValue)
576 fileContent.replace(replacePos, replaceCount, newValue);
580 QStringList MaemoPublisherFremantleFree::findProblems() const
582 QStringList problems;
583 const Qt4Maemo5Target * const target
584 = qobject_cast<Qt4Maemo5Target *>(m_buildConfig->target());
585 const QString &description = target->shortDescription();
586 if (description.trimmed().isEmpty()) {
587 problems << tr("The package description is empty. You must set one "
588 "in Projects -> Run -> Create Package -> Details.");
589 } else if (description.contains(QLatin1String("insert up to"))) {
590 problems << tr("The package description is '%1', which is probably "
591 "not what you want. Please change it in "
592 "Projects -> Run -> Create Package -> Details.").arg(description);
595 if (target->packageManagerIcon(&dummy).isNull())
596 problems << tr("You have not set an icon for the package manager. "
597 "The icon must be set in Projects -> Run -> Create Package -> Details.");
601 void MaemoPublisherFremantleFree::setState(State newState)
603 if (m_state == newState)
605 const State oldState = m_state;
607 if (m_state == Inactive) {
610 case RunningMakeDistclean:
611 case BuildingPackage:
612 disconnect(m_process, 0, this, 0);
613 m_process->terminate();
616 case PreparingToUploadFile:
618 // TODO: Can we ensure the remote scp exits, e.g. by sending
619 // an illegal sequence of bytes? (Probably not, if
620 // we are currently uploading a file.)
621 disconnect(m_uploader.data(), 0, this, 0);
622 m_uploader = SshRemoteProcessRunner::Ptr();
631 } // namespace Internal
632 } // namespace Qt4ProjectManager