OSDN Git Service

Update license.
[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 (info@qt.nokia.com)
8 **
9 **
10 ** GNU Lesser General Public License Usage
11 **
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.
18 **
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.
22 **
23 ** Other Usage
24 **
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.
27 **
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
30 **
31 **************************************************************************/
32 #include "maemopublisherfremantlefree.h"
33
34 #include "maemodeployablelistmodel.h"
35 #include "maemodeploystep.h"
36 #include "maemoglobal.h"
37 #include "maemopackagecreationstep.h"
38 #include "maemopublishingfileselectiondialog.h"
39 #include "qt4maemotarget.h"
40
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>
46
47 #include <QtCore/QCoreApplication>
48 #include <QtCore/QDir>
49 #include <QtCore/QFileInfo>
50 #include <QtCore/QStringList>
51 #include <QtGui/QIcon>
52
53 #define ASSERT_STATE(state) ASSERT_STATE_GENERIC(State, state, m_state)
54
55 using namespace Core;
56 using namespace Utils;
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.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()));
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.userName = 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     MaemoPackageCreationStep::preparePackagingProcess(m_process,
159             m_buildConfig, m_tmpProjectDir);
160     setState(RunningQmake);
161     ProjectExplorer::AbstractProcessStep * const qmakeStep
162         = m_buildConfig->qmakeStep();
163     qmakeStep->init();
164     const ProjectExplorer::ProcessParameters * const pp
165         = qmakeStep->processParameters();
166     m_process->start(pp->effectiveCommand() + QLatin1String(" ")
167         + pp->effectiveArguments());
168 }
169
170 bool MaemoPublisherFremantleFree::copyRecursively(const QString &srcFilePath,
171     const QString &tgtFilePath)
172 {
173     if (m_state == Inactive)
174         return true;
175
176     QFileInfo srcFileInfo(srcFilePath);
177     if (srcFileInfo.isDir()) {
178         if (srcFileInfo == QFileInfo(m_project->projectDirectory()
179                + QLatin1String("/debian")))
180             return true;
181         QString actualSourcePath = srcFilePath;
182         QString actualTargetPath = tgtFilePath;
183
184         if (srcFileInfo.fileName() == QLatin1String("qtc_packaging")) {
185             actualSourcePath += QLatin1String("/debian_fremantle");
186             actualTargetPath.replace(QRegExp(QLatin1String("qtc_packaging$")),
187                 QLatin1String("debian"));
188         }
189
190         QDir targetDir(actualTargetPath);
191         targetDir.cdUp();
192         if (!targetDir.mkdir(QFileInfo(actualTargetPath).fileName())) {
193             emit progressReport(tr("Failed to create directory '%1'.")
194                 .arg(QDir::toNativeSeparators(actualTargetPath)), ErrorOutput);
195             return false;
196         }
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))
203                 return false;
204         }
205     } else {
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)));
210             return false;
211         }
212         QCoreApplication::processEvents();
213
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)));
219                 return false;
220             }
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);
226             rulesFile.resize(0);
227             rulesFile.write(rulesContents);
228         }
229     }
230     return true;
231 }
232
233 bool MaemoPublisherFremantleFree::fixNewlines()
234 {
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))
240             return false;
241         QByteArray contents = file.readAll();
242         const QByteArray crlf("\r\n");
243         if (!contents.contains(crlf))
244             continue;
245         contents.replace(crlf, "\n");
246         file.resize(0);
247         file.write(contents);
248     }
249     return true;
250 }
251
252 void MaemoPublisherFremantleFree::handleProcessError(QProcess::ProcessError error)
253 {
254     if (error == QProcess::FailedToStart)
255         handleProcessFinished(true);
256 }
257
258 void MaemoPublisherFremantleFree::handleProcessFinished()
259 {
260     handleProcessFinished(false);
261 }
262
263 void MaemoPublisherFremantleFree::handleProcessStdOut()
264 {
265     if (m_state == RunningQmake || m_state == RunningMakeDistclean
266             || m_state == BuildingPackage) {
267         emit progressReport(QString::fromLocal8Bit(m_process->readAllStandardOutput()),
268             ToolStatusOutput);
269     }
270 }
271
272 void MaemoPublisherFremantleFree::handleProcessStdErr()
273 {
274     if (m_state == RunningQmake || m_state == RunningMakeDistclean
275             || m_state == BuildingPackage) {
276         emit progressReport(QString::fromLocal8Bit(m_process->readAllStandardError()),
277             ToolErrorOutput);
278     }
279 }
280
281 void MaemoPublisherFremantleFree::handleProcessFinished(bool failedToStart)
282 {
283     ASSERT_STATE(QList<State>() << RunningQmake << RunningMakeDistclean
284         << BuildingPackage << Inactive);
285
286     switch (m_state) {
287     case RunningQmake:
288         if (failedToStart || m_process->exitStatus() != QProcess::NormalExit
289                 ||m_process->exitCode() != 0) {
290             runDpkgBuildPackage();
291         } else {
292             setState(RunningMakeDistclean);
293             m_process->start(m_buildConfig->makeCommand(),
294                 QStringList() << QLatin1String("distclean"));
295         }
296         break;
297     case RunningMakeDistclean:
298         runDpkgBuildPackage();
299         break;
300     case BuildingPackage: {
301         QString error;
302         if (failedToStart) {
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.");
307         }
308
309         if (!error.isEmpty()) {
310             finishWithFailure(error, tr("Package creation failed."));
311             return;
312         }
313
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);
321             else
322                 m_filesToUpload.prepend(filePath);
323         }
324         if (!m_doUpload) {
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"));
332             setState(Inactive);
333         } else {
334             uploadPackage();
335         }
336         break;
337     }
338     default:
339         break;
340     }
341 }
342
343 void MaemoPublisherFremantleFree::runDpkgBuildPackage()
344 {
345     MaemoPublishingFileSelectionDialog d(m_tmpProjectDir);
346     if (d.exec() == QDialog::Rejected) {
347         cancel();
348         return;
349     }
350     foreach (const QString &filePath, d.filesToExclude()) {
351         QString error;
352         if (!MaemoGlobal::removeRecursively(filePath, error)) {
353             finishWithFailure(error,
354                 tr("Publishing failed: Could not create package."));
355         }
356     }
357
358     if (m_state == Inactive)
359         return;
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);
365 }
366
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()
370 {
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());
383 }
384
385 void MaemoPublisherFremantleFree::handleScpStarted()
386 {
387     ASSERT_STATE(QList<State>() << StartingScp << Inactive);
388
389     if (m_state == StartingScp)
390         prepareToSendFile();
391 }
392
393 void MaemoPublisherFremantleFree::handleConnectionError()
394 {
395     if (m_state != Inactive) {
396         finishWithFailure(tr("SSH error: %1").arg(m_uploader->connection()->errorString()),
397             tr("Upload failed."));
398     }
399 }
400
401 void MaemoPublisherFremantleFree::handleUploadJobFinished(int exitStatus)
402 {
403     ASSERT_STATE(QList<State>() << PreparingToUploadFile << UploadingFile
404         << Inactive);
405
406     if (m_state != Inactive && (exitStatus != SshRemoteProcess::ExitedNormally
407             || m_uploader->process()->exitCode() != 0)) {
408         QString error;
409         if (exitStatus != SshRemoteProcess::ExitedNormally) {
410             error = tr("Error uploading file: %1")
411                 .arg(m_uploader->process()->errorString());
412         } else {
413             error = tr("Error uploading file.");
414         }
415         finishWithFailure(error, tr("Upload failed."));
416     }
417 }
418
419 void MaemoPublisherFremantleFree::prepareToSendFile()
420 {
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.");
426         setState(Inactive);
427         return;
428     }
429
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');
437 }
438
439 void MaemoPublisherFremantleFree::sendFile()
440 {
441     Q_ASSERT(!m_filesToUpload.isEmpty());
442     Q_ASSERT(m_state == PreparingToUploadFile);
443
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."));
450         return;
451     }
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."));
459             return;
460         }
461         m_uploader->process()->sendInput(data);
462         bytesToSend -= data.size();
463         QCoreApplication::processEvents();
464         if (m_state == Inactive)
465             return;
466     }
467     m_uploader->process()->sendInput(QByteArray(1, '\0'));
468 }
469
470 void MaemoPublisherFremantleFree::handleScpStdOut(const QByteArray &output)
471 {
472     ASSERT_STATE(QList<State>() << PreparingToUploadFile << UploadingFile
473         << Inactive);
474
475     if (m_state == Inactive)
476         return;
477
478     m_scpOutput += output;
479     if (m_scpOutput == QByteArray(1, '\0')) {
480         m_scpOutput.clear();
481         switch (m_state) {
482         case PreparingToUploadFile:
483             sendFile();
484             break;
485         case UploadingFile:
486             prepareToSendFile();
487             break;
488         default:
489             break;
490         }
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));
497         } else {
498             progressError = tr("Error uploading file.");
499         }
500         finishWithFailure(progressError, tr("Upload failed."));
501     }
502 }
503
504 QString MaemoPublisherFremantleFree::tmpDirContainer() const
505 {
506     return QDir::tempPath() + QLatin1String("/qtc_packaging_")
507         + m_project->displayName();
508 }
509
510 void MaemoPublisherFremantleFree::finishWithFailure(const QString &progressMsg,
511     const QString &resultMsg)
512 {
513     if (!progressMsg.isEmpty())
514         emit progressReport(progressMsg, ErrorOutput);
515     m_resultString = resultMsg;
516     setState(Inactive);
517 }
518
519 bool MaemoPublisherFremantleFree::updateDesktopFiles(QString *error) const
520 {
521     bool success = true;
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())
530             continue;
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()));
537             continue;
538         }
539         if (!desktopFile.exists() || !desktopFile.open(QIODevice::ReadWrite)) {
540             success = false;
541             if (error) {
542                 *error = tr("Failed to adapt desktop file '%1'.")
543                     .arg(desktopFilePath);
544             }
545             continue;
546         }
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);
553         }
554     }
555     return success;
556 }
557
558 bool MaemoPublisherFremantleFree::addOrReplaceDesktopFileValue(QByteArray &fileContent,
559     const QByteArray &key, const QByteArray &newValue) const
560 {
561     const int keyPos = fileContent.indexOf(key + '=');
562     if (keyPos == -1) {
563         if (!fileContent.endsWith('\n'))
564             fileContent += '\n';
565         fileContent += key + '=' + newValue + '\n';
566         return true;
567     }
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)
575         return false;
576     fileContent.replace(replacePos, replaceCount, newValue);
577     return true;
578 }
579
580 QStringList MaemoPublisherFremantleFree::findProblems() const
581 {
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);
593     }
594     QString dummy;
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.");
598     return problems;
599 }
600
601 void MaemoPublisherFremantleFree::setState(State newState)
602 {
603     if (m_state == newState)
604         return;
605     const State oldState = m_state;
606     m_state = newState;
607     if (m_state == Inactive) {
608         switch (oldState) {
609         case RunningQmake:
610         case RunningMakeDistclean:
611         case BuildingPackage:
612             disconnect(m_process, 0, this, 0);
613             m_process->terminate();
614             break;
615         case StartingScp:
616         case PreparingToUploadFile:
617         case UploadingFile:
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();
623             break;
624         default:
625             break;
626         }
627         emit finished();
628     }
629 }
630
631 } // namespace Internal
632 } // namespace Qt4ProjectManager