OSDN Git Service

It's 2011 now.
[qt-creator-jp/qt-creator-jp.git] / src / plugins / qt4projectmanager / qt-maemo / maemopublisherfremantlefree.cpp
1 /**************************************************************************
2 **
3 ** This file is part of Qt Creator
4 **
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
6 **
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
8 **
9 ** No Commercial Usage
10 **
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
14 ** this package.
15 **
16 ** GNU Lesser General Public License Usage
17 **
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.
24 **
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.
28 **
29 ** If you have questions regarding the use of this file, please contact
30 ** Nokia at qt-info@nokia.com.
31 **
32 **************************************************************************/
33 #include "maemopublisherfremantlefree.h"
34
35 #include "maemodeployablelistmodel.h"
36 #include "maemodeploystep.h"
37 #include "maemoglobal.h"
38 #include "maemopackagecreationstep.h"
39 #include "maemopublishingfileselectiondialog.h"
40 #include "maemotemplatesmanager.h"
41
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>
47
48 #include <QtCore/QCoreApplication>
49 #include <QtCore/QDir>
50 #include <QtCore/QFileInfo>
51 #include <QtCore/QStringList>
52 #include <QtGui/QIcon>
53
54 #define ASSERT_STATE(state) ASSERT_STATE_GENERIC(State, state, m_state)
55
56 using namespace Core;
57
58 namespace Qt4ProjectManager {
59 namespace Internal {
60
61 MaemoPublisherFremantleFree::MaemoPublisherFremantleFree(const ProjectExplorer::Project *project,
62     QObject *parent) :
63     QObject(parent),
64     m_project(project),
65     m_state(Inactive),
66     m_sshParams(SshConnectionParameters::DefaultProxy)
67 {
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()));
80 }
81
82 MaemoPublisherFremantleFree::~MaemoPublisherFremantleFree()
83 {
84     ASSERT_STATE(Inactive);
85     m_process->kill();
86 }
87
88 void MaemoPublisherFremantleFree::publish()
89 {
90     createPackage();
91 }
92
93 void MaemoPublisherFremantleFree::setSshParams(const QString &hostName,
94     const QString &userName, const QString &keyFile, const QString &remoteDir)
95 {
96     Q_ASSERT(m_doUpload);
97     m_sshParams.host = hostName;
98     m_sshParams.uname = userName;
99     m_sshParams.privateKeyFile = keyFile;
100     m_remoteDir = remoteDir;
101 }
102
103 void MaemoPublisherFremantleFree::cancel()
104 {
105     finishWithFailure(tr("Canceled."), tr("Publishing canceled by user."));
106 }
107
108 void MaemoPublisherFremantleFree::createPackage()
109 {
110     setState(CopyingProjectDir);
111
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."));
118         return;
119     }
120
121     m_tmpProjectDir = tmpDirContainer() + QLatin1Char('/')
122         + m_project->displayName();
123     if (QFileInfo(tmpDirContainer()).exists()) {
124         emit progressReport(tr("Removing left-over temporary directory ..."));
125         QString error;
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."));
129             return;
130         }
131     }
132
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."));
137         return;
138     }
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."));
142         return;
143     }
144     if (!fixNewlines()) {
145         finishWithFailure(tr("Error: Could not fix newlines"),
146             tr("Publishing failed: Could not create source package."));
147         return;
148     }
149
150     QString error;
151     if (!updateDesktopFiles(&error)) {
152         finishWithFailure(error,
153             tr("Publishing failed: Could not create package."));
154         return;
155     }
156
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."));
162         return;
163     }
164
165     setState(RunningQmake);
166     ProjectExplorer::AbstractProcessStep * const qmakeStep
167         = m_buildConfig->qmakeStep();
168     qmakeStep->init();
169     const ProjectExplorer::ProcessParameters * const pp
170         = qmakeStep->processParameters();
171     m_process->start(pp->effectiveCommand() + QLatin1String(" ")
172         + pp->effectiveArguments());
173 }
174
175 bool MaemoPublisherFremantleFree::copyRecursively(const QString &srcFilePath,
176     const QString &tgtFilePath)
177 {
178     if (m_state == Inactive)
179         return true;
180
181     QFileInfo srcFileInfo(srcFilePath);
182     if (srcFileInfo.isDir()) {
183         if (srcFileInfo == QFileInfo(m_project->projectDirectory()
184                + QLatin1String("/debian")))
185             return true;
186         QString actualSourcePath = srcFilePath;
187         QString actualTargetPath = tgtFilePath;
188
189         if (srcFileInfo.fileName() == QLatin1String("qtc_packaging")) {
190             actualSourcePath += QLatin1String("/debian_fremantle");
191             actualTargetPath.replace(QRegExp(QLatin1String("qtc_packaging$")),
192                 QLatin1String("debian"));
193         }
194
195         QDir targetDir(actualTargetPath);
196         targetDir.cdUp();
197         if (!targetDir.mkdir(QFileInfo(actualTargetPath).fileName())) {
198             emit progressReport(tr("Failed to create directory '%1'.")
199                 .arg(QDir::toNativeSeparators(actualTargetPath)), ErrorOutput);
200             return false;
201         }
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))
208                 return false;
209         }
210     } else {
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)));
215             return false;
216         }
217         QCoreApplication::processEvents();
218
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)));
224                 return false;
225             }
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);
231             rulesFile.resize(0);
232             rulesFile.write(rulesContents);
233         }
234     }
235     return true;
236 }
237
238 bool MaemoPublisherFremantleFree::fixNewlines()
239 {
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))
245             return false;
246         QByteArray contents = file.readAll();
247         const QByteArray crlf("\r\n");
248         if (!contents.contains(crlf))
249             continue;
250         contents.replace(crlf, "\n");
251         file.resize(0);
252         file.write(contents);
253     }
254     return true;
255 }
256
257 void MaemoPublisherFremantleFree::handleProcessError(QProcess::ProcessError error)
258 {
259     if (error == QProcess::FailedToStart)
260         handleProcessFinished(true);
261 }
262
263 void MaemoPublisherFremantleFree::handleProcessFinished()
264 {
265     handleProcessFinished(false);
266 }
267
268 void MaemoPublisherFremantleFree::handleProcessStdOut()
269 {
270     if (m_state == RunningQmake || m_state == RunningMakeDistclean
271             || m_state == BuildingPackage) {
272         emit progressReport(QString::fromLocal8Bit(m_process->readAllStandardOutput()),
273             ToolStatusOutput);
274     }
275 }
276
277 void MaemoPublisherFremantleFree::handleProcessStdErr()
278 {
279     if (m_state == RunningQmake || m_state == RunningMakeDistclean
280             || m_state == BuildingPackage) {
281         emit progressReport(QString::fromLocal8Bit(m_process->readAllStandardError()),
282             ToolErrorOutput);
283     }
284 }
285
286 void MaemoPublisherFremantleFree::handleProcessFinished(bool failedToStart)
287 {
288     ASSERT_STATE(QList<State>() << RunningQmake << RunningMakeDistclean
289         << BuildingPackage << Inactive);
290
291     switch (m_state) {
292     case RunningQmake:
293         if (failedToStart || m_process->exitStatus() != QProcess::NormalExit
294                 ||m_process->exitCode() != 0) {
295             runDpkgBuildPackage();
296         } else {
297             setState(RunningMakeDistclean);
298             m_process->start(m_buildConfig->makeCommand(),
299                 QStringList() << QLatin1String("distclean"));
300         }
301         break;
302     case RunningMakeDistclean:
303         runDpkgBuildPackage();
304         break;
305     case BuildingPackage: {
306         QString error;
307         if (failedToStart) {
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.");
312         }
313
314         if (!error.isEmpty()) {
315             finishWithFailure(error, tr("Package creation failed."));
316             return;
317         }
318
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);
326             else
327                 m_filesToUpload.prepend(filePath);
328         }
329         if (!m_doUpload) {
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"));
337             setState(Inactive);
338         } else {
339             uploadPackage();
340         }
341         break;
342     }
343     default:
344         break;
345     }
346 }
347
348 void MaemoPublisherFremantleFree::runDpkgBuildPackage()
349 {
350     MaemoPublishingFileSelectionDialog d(m_tmpProjectDir);
351     if (d.exec() == QDialog::Rejected) {
352         cancel();
353         return;
354     }
355     foreach (const QString &filePath, d.filesToExclude()) {
356         QString error;
357         if (!MaemoGlobal::removeRecursively(filePath, error)) {
358             finishWithFailure(error,
359                 tr("Publishing failed: Could not create package."));
360         }
361     }
362
363     if (m_state == Inactive)
364         return;
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());
372 }
373
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()
377 {
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());
390 }
391
392 void MaemoPublisherFremantleFree::handleScpStarted()
393 {
394     ASSERT_STATE(QList<State>() << StartingScp << Inactive);
395
396     if (m_state == StartingScp)
397         prepareToSendFile();
398 }
399
400 void MaemoPublisherFremantleFree::handleConnectionError()
401 {
402     if (m_state != Inactive) {
403         finishWithFailure(tr("SSH error: %1").arg(m_uploader->connection()->errorString()),
404             tr("Upload failed."));
405     }
406 }
407
408 void MaemoPublisherFremantleFree::handleUploadJobFinished(int exitStatus)
409 {
410     ASSERT_STATE(QList<State>() << PreparingToUploadFile << UploadingFile
411         << Inactive);
412
413     if (m_state != Inactive && (exitStatus != SshRemoteProcess::ExitedNormally
414             || m_uploader->process()->exitCode() != 0)) {
415         QString error;
416         if (exitStatus != SshRemoteProcess::ExitedNormally) {
417             error = tr("Error uploading file: %1")
418                 .arg(m_uploader->process()->errorString());
419         } else {
420             error = tr("Error uploading file.");
421         }
422         finishWithFailure(error, tr("Upload failed."));
423     }
424 }
425
426 void MaemoPublisherFremantleFree::prepareToSendFile()
427 {
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.");
433         setState(Inactive);
434         return;
435     }
436
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');
444 }
445
446 void MaemoPublisherFremantleFree::sendFile()
447 {
448     Q_ASSERT(!m_filesToUpload.isEmpty());
449     Q_ASSERT(m_state == PreparingToUploadFile);
450
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."));
457         return;
458     }
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."));
466             return;
467         }
468         m_uploader->process()->sendInput(data);
469         bytesToSend -= data.size();
470         QCoreApplication::processEvents();
471         if (m_state == Inactive)
472             return;
473     }
474     m_uploader->process()->sendInput(QByteArray(1, '\0'));
475 }
476
477 void MaemoPublisherFremantleFree::handleScpStdOut(const QByteArray &output)
478 {
479     ASSERT_STATE(QList<State>() << PreparingToUploadFile << UploadingFile
480         << Inactive);
481
482     if (m_state == Inactive)
483         return;
484
485     m_scpOutput += output;
486     if (m_scpOutput == QByteArray(1, '\0')) {
487         m_scpOutput.clear();
488         switch (m_state) {
489         case PreparingToUploadFile:
490             sendFile();
491             break;
492         case UploadingFile:
493             prepareToSendFile();
494             break;
495         default:
496             break;
497         }
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));
504         } else {
505             progressError = tr("Error uploading file.");
506         }
507         finishWithFailure(progressError, tr("Upload failed."));
508     }
509 }
510
511 QString MaemoPublisherFremantleFree::tmpDirContainer() const
512 {
513     return QDir::tempPath() + QLatin1String("/qtc_packaging_")
514         + m_project->displayName();
515 }
516
517 void MaemoPublisherFremantleFree::finishWithFailure(const QString &progressMsg,
518     const QString &resultMsg)
519 {
520     if (!progressMsg.isEmpty())
521         emit progressReport(progressMsg, ErrorOutput);
522     m_resultString = resultMsg;
523     setState(Inactive);
524 }
525
526 bool MaemoPublisherFremantleFree::updateDesktopFiles(QString *error) const
527 {
528     bool success = true;
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())
537             continue;
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()));
544             continue;
545         }
546         if (!desktopFile.exists() || !desktopFile.open(QIODevice::ReadWrite)) {
547             success = false;
548             if (error) {
549                 *error = tr("Failed to adapt desktop file '%1'.")
550                     .arg(desktopFilePath);
551             }
552             continue;
553         }
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);
560         }
561     }
562     return success;
563 }
564
565 bool MaemoPublisherFremantleFree::addOrReplaceDesktopFileValue(QByteArray &fileContent,
566     const QByteArray &key, const QByteArray &newValue) const
567 {
568     const int keyPos = fileContent.indexOf(key + '=');
569     if (keyPos == -1) {
570         if (!fileContent.endsWith('\n'))
571             fileContent += '\n';
572         fileContent += key + '=' + newValue + '\n';
573         return true;
574     }
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)
582         return false;
583     fileContent.replace(replacePos, replaceCount, newValue);
584     return true;
585 }
586
587 QStringList MaemoPublisherFremantleFree::findProblems() const
588 {
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);
598     }
599     QString dummy;
600     if (templatesManager->packageManagerIcon(m_project, &dummy).isNull())
601         problems << tr("You have not set an icon for the package manager.");
602     return problems;
603 }
604
605 void MaemoPublisherFremantleFree::setState(State newState)
606 {
607     if (m_state == newState)
608         return;
609     const State oldState = m_state;
610     m_state = newState;
611     if (m_state == Inactive) {
612         switch (oldState) {
613         case RunningQmake:
614         case RunningMakeDistclean:
615         case BuildingPackage:
616             disconnect(m_process, 0, this, 0);
617             m_process->terminate();
618             break;
619         case StartingScp:
620         case PreparingToUploadFile:
621         case UploadingFile:
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();
627             break;
628         default:
629             break;
630         }
631         emit finished();
632     }
633 }
634
635 } // namespace Internal
636 } // namespace Qt4ProjectManager