1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
11 ** This file contains pre-release code and may not be distributed.
12 ** You may use this file in accordance with the terms and conditions
13 ** contained in the Technology Preview License Agreement accompanying
16 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 ** In addition, as a special exception, Nokia gives you certain additional
26 ** rights. These rights are described in the Nokia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 ** If you have questions regarding the use of this file, please contact
30 ** Nokia at qt-info@nokia.com.
32 **************************************************************************/
33 #include "maemopublisherfremantlefree.h"
35 #include "maemodeployablelistmodel.h"
36 #include "maemodeploystep.h"
37 #include "maemoglobal.h"
38 #include "maemopackagecreationstep.h"
39 #include "maemopublishingfileselectiondialog.h"
40 #include "maemotemplatesmanager.h"
42 #include <coreplugin/ifile.h>
43 #include <projectexplorer/project.h>
44 #include <projectexplorer/target.h>
45 #include <qt4projectmanager/qmakestep.h>
46 #include <qt4projectmanager/qt4buildconfiguration.h>
48 #include <QtCore/QCoreApplication>
49 #include <QtCore/QDir>
50 #include <QtCore/QFileInfo>
51 #include <QtCore/QStringList>
52 #include <QtGui/QIcon>
54 #define ASSERT_STATE(state) ASSERT_STATE_GENERIC(State, state, m_state)
58 namespace Qt4ProjectManager {
61 MaemoPublisherFremantleFree::MaemoPublisherFremantleFree(const ProjectExplorer::Project *project,
66 m_sshParams(SshConnectionParameters::DefaultProxy)
68 m_sshParams.authType = SshConnectionParameters::AuthByKey;
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.uname = 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 if (!MaemoPackageCreationStep::preparePackagingProcess(m_process,
159 m_buildConfig, m_tmpProjectDir, &error)) {
160 finishWithFailure(tr("Error preparing packaging process: %1").arg(error),
161 tr("Publishing failed: Could not create package."));
165 setState(RunningQmake);
166 ProjectExplorer::AbstractProcessStep * const qmakeStep
167 = m_buildConfig->qmakeStep();
169 const ProjectExplorer::ProcessParameters * const pp
170 = qmakeStep->processParameters();
171 m_process->start(pp->effectiveCommand() + QLatin1String(" ")
172 + pp->effectiveArguments());
175 bool MaemoPublisherFremantleFree::copyRecursively(const QString &srcFilePath,
176 const QString &tgtFilePath)
178 if (m_state == Inactive)
181 QFileInfo srcFileInfo(srcFilePath);
182 if (srcFileInfo.isDir()) {
183 if (srcFileInfo == QFileInfo(m_project->projectDirectory()
184 + QLatin1String("/debian")))
186 QString actualSourcePath = srcFilePath;
187 QString actualTargetPath = tgtFilePath;
189 if (srcFileInfo.fileName() == QLatin1String("qtc_packaging")) {
190 actualSourcePath += QLatin1String("/debian_fremantle");
191 actualTargetPath.replace(QRegExp(QLatin1String("qtc_packaging$")),
192 QLatin1String("debian"));
195 QDir targetDir(actualTargetPath);
197 if (!targetDir.mkdir(QFileInfo(actualTargetPath).fileName())) {
198 emit progressReport(tr("Failed to create directory '%1'.")
199 .arg(QDir::toNativeSeparators(actualTargetPath)), ErrorOutput);
202 QDir sourceDir(actualSourcePath);
203 QStringList fileNames = sourceDir.entryList(QDir::Files | QDir::Hidden
204 | QDir::System | QDir::Dirs | QDir::NoDotAndDotDot);
205 foreach (const QString &fileName, fileNames) {
206 if (!copyRecursively(actualSourcePath + QLatin1Char('/') + fileName,
207 actualTargetPath + QLatin1Char('/') + fileName))
211 if (!QFile::copy(srcFilePath, tgtFilePath)) {
212 emit progressReport(tr("Could not copy file '%1' to '%2'.")
213 .arg(QDir::toNativeSeparators(srcFilePath),
214 QDir::toNativeSeparators(tgtFilePath)));
217 QCoreApplication::processEvents();
219 if (tgtFilePath == m_tmpProjectDir + QLatin1String("/debian/rules")) {
220 QFile rulesFile(tgtFilePath);
221 if (!rulesFile.open(QIODevice::ReadWrite)) {
222 emit progressReport(tr("Error: Cannot open file '%1'.")
223 .arg(QDir::toNativeSeparators(tgtFilePath)));
226 QByteArray rulesContents = rulesFile.readAll();
227 rulesContents.replace("$(MAKE) clean", "# $(MAKE) clean");
228 rulesContents.replace("# Add here commands to configure the package.",
229 "qmake " + QFileInfo(m_project->file()->fileName()).fileName().toLocal8Bit());
230 MaemoPackageCreationStep::ensureShlibdeps(rulesContents);
232 rulesFile.write(rulesContents);
238 bool MaemoPublisherFremantleFree::fixNewlines()
240 QDir debianDir(m_tmpProjectDir + QLatin1String("/debian"));
241 const QStringList &fileNames = debianDir.entryList(QDir::Files);
242 foreach (const QString &fileName, fileNames) {
243 QFile file(debianDir.filePath(fileName));
244 if (!file.open(QIODevice::ReadWrite))
246 QByteArray contents = file.readAll();
247 const QByteArray crlf("\r\n");
248 if (!contents.contains(crlf))
250 contents.replace(crlf, "\n");
252 file.write(contents);
257 void MaemoPublisherFremantleFree::handleProcessError(QProcess::ProcessError error)
259 if (error == QProcess::FailedToStart)
260 handleProcessFinished(true);
263 void MaemoPublisherFremantleFree::handleProcessFinished()
265 handleProcessFinished(false);
268 void MaemoPublisherFremantleFree::handleProcessStdOut()
270 if (m_state == RunningQmake || m_state == RunningMakeDistclean
271 || m_state == BuildingPackage) {
272 emit progressReport(QString::fromLocal8Bit(m_process->readAllStandardOutput()),
277 void MaemoPublisherFremantleFree::handleProcessStdErr()
279 if (m_state == RunningQmake || m_state == RunningMakeDistclean
280 || m_state == BuildingPackage) {
281 emit progressReport(QString::fromLocal8Bit(m_process->readAllStandardError()),
286 void MaemoPublisherFremantleFree::handleProcessFinished(bool failedToStart)
288 ASSERT_STATE(QList<State>() << RunningQmake << RunningMakeDistclean
289 << BuildingPackage << Inactive);
293 if (failedToStart || m_process->exitStatus() != QProcess::NormalExit
294 ||m_process->exitCode() != 0) {
295 runDpkgBuildPackage();
297 setState(RunningMakeDistclean);
298 m_process->start(m_buildConfig->makeCommand(),
299 QStringList() << QLatin1String("distclean"));
302 case RunningMakeDistclean:
303 runDpkgBuildPackage();
305 case BuildingPackage: {
308 error = tr("Error: Failed to start dpkg-buildpackage.");
309 } else if (m_process->exitStatus() != QProcess::NormalExit
310 || m_process->exitCode() != 0) {
311 error = tr("Error: dpkg-buildpackage did not succeed.");
314 if (!error.isEmpty()) {
315 finishWithFailure(error, tr("Package creation failed."));
319 QDir dir(tmpDirContainer());
320 const QStringList &fileNames = dir.entryList(QDir::Files);
321 foreach (const QString &fileName, fileNames) {
322 const QString filePath
323 = tmpDirContainer() + QLatin1Char('/') + fileName;
324 if (fileName.endsWith(QLatin1String(".dsc")))
325 m_filesToUpload.append(filePath);
327 m_filesToUpload.prepend(filePath);
330 emit progressReport(tr("Done."));
331 QStringList nativeFilePaths;
332 foreach (const QString &filePath, m_filesToUpload)
333 nativeFilePaths << QDir::toNativeSeparators(filePath);
334 m_resultString = tr("Packaging finished successfully. "
335 "The following files were created:\n")
336 + nativeFilePaths.join(QLatin1String("\n"));
348 void MaemoPublisherFremantleFree::runDpkgBuildPackage()
350 MaemoPublishingFileSelectionDialog d(m_tmpProjectDir);
351 if (d.exec() == QDialog::Rejected) {
355 foreach (const QString &filePath, d.filesToExclude()) {
357 if (!MaemoGlobal::removeRecursively(filePath, error)) {
358 finishWithFailure(error,
359 tr("Publishing failed: Could not create package."));
363 if (m_state == Inactive)
365 setState(BuildingPackage);
366 emit progressReport(tr("Building source package..."));
367 const QStringList args = QStringList() << QLatin1String("-t")
368 << MaemoGlobal::targetName(m_buildConfig->qtVersion())
369 << QLatin1String("dpkg-buildpackage") << QLatin1String("-S")
370 << QLatin1String("-us") << QLatin1String("-uc");
371 MaemoGlobal::callMad(*m_process, args, m_buildConfig->qtVersion());
374 // We have to implement the SCP protocol, because the maemo.org
375 // webmaster refuses to enable SFTP "for security reasons" ...
376 void MaemoPublisherFremantleFree::uploadPackage()
378 m_uploader = SshRemoteProcessRunner::create(m_sshParams);
379 connect(m_uploader.data(), SIGNAL(processStarted()),
380 SLOT(handleScpStarted()));
381 connect(m_uploader.data(), SIGNAL(connectionError(Core::SshError)),
382 SLOT(handleConnectionError()));
383 connect(m_uploader.data(), SIGNAL(processClosed(int)),
384 SLOT(handleUploadJobFinished(int)));
385 connect(m_uploader.data(), SIGNAL(processOutputAvailable(QByteArray)),
386 SLOT(handleScpStdOut(QByteArray)));
387 emit progressReport(tr("Starting scp ..."));
388 setState(StartingScp);
389 m_uploader->run("scp -td " + m_remoteDir.toUtf8());
392 void MaemoPublisherFremantleFree::handleScpStarted()
394 ASSERT_STATE(QList<State>() << StartingScp << Inactive);
396 if (m_state == StartingScp)
400 void MaemoPublisherFremantleFree::handleConnectionError()
402 if (m_state != Inactive) {
403 finishWithFailure(tr("SSH error: %1").arg(m_uploader->connection()->errorString()),
404 tr("Upload failed."));
408 void MaemoPublisherFremantleFree::handleUploadJobFinished(int exitStatus)
410 ASSERT_STATE(QList<State>() << PreparingToUploadFile << UploadingFile
413 if (m_state != Inactive && (exitStatus != SshRemoteProcess::ExitedNormally
414 || m_uploader->process()->exitCode() != 0)) {
416 if (exitStatus != SshRemoteProcess::ExitedNormally) {
417 error = tr("Error uploading file: %1")
418 .arg(m_uploader->process()->errorString());
420 error = tr("Error uploading file.");
422 finishWithFailure(error, tr("Upload failed."));
426 void MaemoPublisherFremantleFree::prepareToSendFile()
428 if (m_filesToUpload.isEmpty()) {
429 emit progressReport(tr("All files uploaded."));
430 m_resultString = tr("Upload succeeded. You should shortly "
431 "receive an email informing you about the outcome "
432 "of the build process.");
437 setState(PreparingToUploadFile);
438 const QString &nextFilePath = m_filesToUpload.first();
439 emit progressReport(tr("Uploading file %1 ...")
440 .arg(QDir::toNativeSeparators(nextFilePath)));
441 QFileInfo info(nextFilePath);
442 m_uploader->process()->sendInput("C0644 " + QByteArray::number(info.size())
443 + ' ' + info.fileName().toUtf8() + '\n');
446 void MaemoPublisherFremantleFree::sendFile()
448 Q_ASSERT(!m_filesToUpload.isEmpty());
449 Q_ASSERT(m_state == PreparingToUploadFile);
451 setState(UploadingFile);
452 const QString filePath = m_filesToUpload.takeFirst();
453 QFile file(filePath);
454 if (!file.open(QIODevice::ReadOnly)) {
455 finishWithFailure(tr("Cannot open file for reading: %1")
456 .arg(file.errorString()), tr("Upload failed."));
459 qint64 bytesToSend = file.size();
460 while (bytesToSend > 0) {
461 const QByteArray &data
462 = file.read(qMin(bytesToSend, Q_INT64_C(1024*1024)));
463 if (data.count() == 0) {
464 finishWithFailure(tr("Cannot read file: %1").arg(file.errorString()),
465 tr("Upload failed."));
468 m_uploader->process()->sendInput(data);
469 bytesToSend -= data.size();
470 QCoreApplication::processEvents();
471 if (m_state == Inactive)
474 m_uploader->process()->sendInput(QByteArray(1, '\0'));
477 void MaemoPublisherFremantleFree::handleScpStdOut(const QByteArray &output)
479 ASSERT_STATE(QList<State>() << PreparingToUploadFile << UploadingFile
482 if (m_state == Inactive)
485 m_scpOutput += output;
486 if (m_scpOutput == QByteArray(1, '\0')) {
489 case PreparingToUploadFile:
498 } else if (m_scpOutput.endsWith('\n')) {
499 const QByteArray error = m_scpOutput.mid(1, m_scpOutput.count() - 2);
500 QString progressError;
501 if (!error.isEmpty()) {
502 progressError = tr("Error uploading file: %1")
503 .arg(QString::fromUtf8(error));
505 progressError = tr("Error uploading file.");
507 finishWithFailure(progressError, tr("Upload failed."));
511 QString MaemoPublisherFremantleFree::tmpDirContainer() const
513 return QDir::tempPath() + QLatin1String("/qtc_packaging_")
514 + m_project->displayName();
517 void MaemoPublisherFremantleFree::finishWithFailure(const QString &progressMsg,
518 const QString &resultMsg)
520 if (!progressMsg.isEmpty())
521 emit progressReport(progressMsg, ErrorOutput);
522 m_resultString = resultMsg;
526 bool MaemoPublisherFremantleFree::updateDesktopFiles(QString *error) const
529 MaemoDeployStep * const deployStep
530 = MaemoGlobal::buildStep<MaemoDeployStep>(m_buildConfig->target()
531 ->activeDeployConfiguration());
532 for (int i = 0; i < deployStep->deployables()->modelCount(); ++i) {
533 const MaemoDeployableListModel * const model
534 = deployStep->deployables()->modelAt(i);
535 QString desktopFilePath = model->localDesktopFilePath();
536 if (desktopFilePath.isEmpty())
538 desktopFilePath.replace(model->projectDir(), m_tmpProjectDir);
539 QFile desktopFile(desktopFilePath);
540 const QString executableFilePath = model->remoteExecutableFilePath();
541 if (executableFilePath.isEmpty()) {
542 qDebug("%s: Skipping subproject %s with missing deployment information.",
543 Q_FUNC_INFO, qPrintable(model->proFilePath()));
546 if (!desktopFile.exists() || !desktopFile.open(QIODevice::ReadWrite)) {
549 *error = tr("Failed to adapt desktop file '%1'.")
550 .arg(desktopFilePath);
554 QByteArray desktopFileContents = desktopFile.readAll();
555 bool fileNeedsUpdate = addOrReplaceDesktopFileValue(desktopFileContents,
556 "Exec", executableFilePath.toUtf8());
557 if (fileNeedsUpdate) {
558 desktopFile.resize(0);
559 desktopFile.write(desktopFileContents);
565 bool MaemoPublisherFremantleFree::addOrReplaceDesktopFileValue(QByteArray &fileContent,
566 const QByteArray &key, const QByteArray &newValue) const
568 const int keyPos = fileContent.indexOf(key + '=');
570 if (!fileContent.endsWith('\n'))
572 fileContent += key + '=' + newValue + '\n';
575 int nextNewlinePos = fileContent.indexOf('\n', keyPos);
576 if (nextNewlinePos == -1)
577 nextNewlinePos = fileContent.count();
578 const int replacePos = keyPos + key.count() + 1;
579 const int replaceCount = nextNewlinePos - replacePos;
580 const QByteArray &oldValue = fileContent.mid(replacePos, replaceCount);
581 if (oldValue == newValue)
583 fileContent.replace(replacePos, replaceCount, newValue);
587 QStringList MaemoPublisherFremantleFree::findProblems() const
589 QStringList problems;
590 const MaemoTemplatesManager * const templatesManager
591 = MaemoTemplatesManager::instance();
592 const QString &description = templatesManager->shortDescription(m_project);
593 if (description.trimmed().isEmpty()) {
594 problems << tr("The package description is empty.");
595 } else if (description.contains(QLatin1String("insert up to"))) {
596 problems << tr("The package description is '%1', which is probably "
597 "not what you want.").arg(description);
600 if (templatesManager->packageManagerIcon(m_project, &dummy).isNull())
601 problems << tr("You have not set an icon for the package manager.");
605 void MaemoPublisherFremantleFree::setState(State newState)
607 if (m_state == newState)
609 const State oldState = m_state;
611 if (m_state == Inactive) {
614 case RunningMakeDistclean:
615 case BuildingPackage:
616 disconnect(m_process, 0, this, 0);
617 m_process->terminate();
620 case PreparingToUploadFile:
622 // TODO: Can we ensure the remote scp exits, e.g. by sending
623 // an illegal sequence of bytes? (Probably not, if
624 // we are currently uploading a file.)
625 disconnect(m_uploader.data(), 0, this, 0);
626 m_uploader = SshRemoteProcessRunner::Ptr();
635 } // namespace Internal
636 } // namespace Qt4ProjectManager