// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "tarpackagecreationstep.h" #include "deploymenttimeinfo.h" #include "remotelinux_constants.h" #include "remotelinuxtr.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ProjectExplorer; using namespace Utils; namespace RemoteLinux::Internal { const int TarBlockSize = 512; struct TarFileHeader { char fileName[100]; char fileMode[8]; char uid[8]; char gid[8]; char length[12]; char mtime[12]; char chksum[8]; char typeflag; char linkname[100]; char magic[6]; char version[2]; char uname[32]; char gname[32]; char devmajor[8]; char devminor[8]; char fileNamePrefix[155]; char padding[12]; }; class TarPackageCreationStep : public BuildStep { public: TarPackageCreationStep(BuildStepList *bsl, Id id); FilePath packageFilePath() const; private: bool init() final; Tasking::GroupItem runRecipe() final; void fromMap(const Storage &map) final; void toMap(Storage &map) const final; QVariant data(Id id) const final; void raiseError(const QString &errorMessage); void raiseWarning(const QString &warningMessage); bool isPackagingNeeded() const; void deployFinished(bool success); void addNeededDeploymentFiles(const DeployableFile &deployable, const Kit *kit); void doPackage(QPromise &promise, const Utils::FilePath &tarFilePath, bool ignoreMissingFiles); bool appendFile(QPromise &promise, QFile &tarFile, const QFileInfo &fileInfo, const QString &remoteFilePath, const Utils::FilePath &tarFilePath, bool ignoreMissingFiles); FilePath m_tarFilePath; bool m_deploymentDataModified = false; DeploymentTimeInfo m_deployTimes; BoolAspect m_incrementalDeployment{this}; BoolAspect m_ignoreMissingFiles{this}; bool m_packagingNeeded = false; QList m_files; FutureSynchronizer m_synchronizer; }; TarPackageCreationStep::TarPackageCreationStep(BuildStepList *bsl, Id id) : BuildStep(bsl, id) { connect(target(), &Target::deploymentDataChanged, this, [this] { m_deploymentDataModified = true; }); m_deploymentDataModified = true; m_incrementalDeployment.setSettingsKey( "RemoteLinux.TarPackageCreationStep.IncrementalDeployment"); m_incrementalDeployment.setLabelText(Tr::tr("Package modified files only")); m_incrementalDeployment.setLabelPlacement(BoolAspect::LabelPlacement::AtCheckBox); m_ignoreMissingFiles.setSettingsKey("RemoteLinux.TarPackageCreationStep.IgnoreMissingFiles"); m_ignoreMissingFiles.setLabelText(Tr::tr("Ignore missing files")); m_ignoreMissingFiles.setLabelPlacement(BoolAspect::LabelPlacement::AtCheckBox); setSummaryUpdater([this] { FilePath path = packageFilePath(); if (path.isEmpty()) return QString("" + Tr::tr("Tarball creation not possible.") + ""); return QString("" + Tr::tr("Create tarball:") + " " + path.toUserOutput()); }); } FilePath TarPackageCreationStep::packageFilePath() const { if (buildDirectory().isEmpty()) return {}; const QString packageFileName = project()->displayName() + ".tar"; return buildDirectory().pathAppended(packageFileName); } bool TarPackageCreationStep::init() { m_tarFilePath = packageFilePath(); m_packagingNeeded = isPackagingNeeded(); return true; } Tasking::GroupItem TarPackageCreationStep::runRecipe() { using namespace Tasking; const auto onSetup = [this](Async &async) { const QList &files = target()->deploymentData().allFiles(); if (m_incrementalDeployment()) { m_files.clear(); for (const DeployableFile &file : files) addNeededDeploymentFiles(file, kit()); } else { m_files = files; } emit addOutput(Tr::tr("Creating tarball..."), OutputFormat::NormalMessage); if (!m_packagingNeeded) { emit addOutput(Tr::tr("Tarball up to date, skipping packaging."), OutputFormat::NormalMessage); return SetupResult::StopWithDone; } async.setConcurrentCallData(&TarPackageCreationStep::doPackage, this, m_tarFilePath, m_ignoreMissingFiles()); async.setFutureSynchronizer(&m_synchronizer); return SetupResult::Continue; }; const auto onDone = [this](const Async &) { m_deploymentDataModified = false; emit addOutput(Tr::tr("Packaging finished successfully."), OutputFormat::NormalMessage); // TODO: Should it be the next task in sequence? connect(BuildManager::instance(), &BuildManager::buildQueueFinished, this, &TarPackageCreationStep::deployFinished); }; const auto onError = [this](const Async &) { emit addOutput(Tr::tr("Packaging failed."), OutputFormat::ErrorMessage); }; return AsyncTask(onSetup, onDone, onError); } void TarPackageCreationStep::fromMap(const Storage &map) { BuildStep::fromMap(map); if (hasError()) return; m_deployTimes.importDeployTimes(map); } void TarPackageCreationStep::toMap(Storage &map) const { BuildStep::toMap(map); map.insert(m_deployTimes.exportDeployTimes()); } QVariant TarPackageCreationStep::data(Id id) const { if (id == Constants::TarPackageFilePathId) return packageFilePath().toVariant(); return {}; } void TarPackageCreationStep::raiseError(const QString &errorMessage) { emit addTask(DeploymentTask(Task::Error, errorMessage)); emit addOutput(errorMessage, OutputFormat::Stderr); } void TarPackageCreationStep::raiseWarning(const QString &warningMessage) { emit addTask(DeploymentTask(Task::Warning, warningMessage)); emit addOutput(warningMessage, OutputFormat::ErrorMessage); } bool TarPackageCreationStep::isPackagingNeeded() const { const FilePath packagePath = packageFilePath(); if (!packagePath.exists() || m_deploymentDataModified) return true; const DeploymentData &dd = target()->deploymentData(); for (int i = 0; i < dd.fileCount(); ++i) { if (dd.fileAt(i).localFilePath().isNewerThan(packagePath.lastModified())) return true; } return false; } void TarPackageCreationStep::deployFinished(bool success) { disconnect(BuildManager::instance(), &BuildManager::buildQueueFinished, this, &TarPackageCreationStep::deployFinished); if (!success) return; const Kit *kit = target()->kit(); // Store files that have been tar'd and successfully deployed for (const DeployableFile &file : std::as_const(m_files)) m_deployTimes.saveDeploymentTimeStamp(file, kit, QDateTime()); } void TarPackageCreationStep::addNeededDeploymentFiles( const DeployableFile &deployable, const Kit *kit) { const QFileInfo fileInfo = deployable.localFilePath().toFileInfo(); if (!fileInfo.isDir()) { if (m_deployTimes.hasLocalFileChanged(deployable, kit)) m_files << deployable; return; } const QStringList files = QDir(deployable.localFilePath().toString()) .entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); if (files.isEmpty()) { m_files << deployable; return; } for (const QString &fileName : files) { const FilePath localFilePath = deployable.localFilePath().pathAppended(fileName); const QString remoteDir = deployable.remoteDirectory() + '/' + fileInfo.fileName(); // Recurse through the subdirectories addNeededDeploymentFiles(DeployableFile(localFilePath, remoteDir), kit); } } // TODO: Fix error / message reporting. Currently, the messages may still be posted // (and delivered) after the async task was already canceled. void TarPackageCreationStep::doPackage(QPromise &promise, const FilePath &tarFilePath, bool ignoreMissingFiles) { // TODO: Optimization: Only package changed files QFile tarFile(tarFilePath.toFSPathString()); if (!tarFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { raiseError(Tr::tr("Error: tar file %1 cannot be opened (%2).") .arg(tarFilePath.toUserOutput(), tarFile.errorString())); promise.future().cancel(); return; } for (const DeployableFile &d : std::as_const(m_files)) { if (d.remoteDirectory().isEmpty()) { emit addOutput(Tr::tr("No remote path specified for file \"%1\", skipping.") .arg(d.localFilePath().toUserOutput()), OutputFormat::ErrorMessage); continue; } QFileInfo fileInfo = d.localFilePath().toFileInfo(); if (!appendFile(promise, tarFile, fileInfo, d.remoteDirectory() + QLatin1Char('/') + fileInfo.fileName(), tarFilePath, ignoreMissingFiles)) { promise.future().cancel(); return; } } const QByteArray eofIndicator(2 * sizeof(TarFileHeader), 0); if (tarFile.write(eofIndicator) != eofIndicator.length()) { raiseError(Tr::tr("Error writing tar file \"%1\": %2.") .arg(QDir::toNativeSeparators(tarFile.fileName()), tarFile.errorString())); promise.future().cancel(); return; } } static bool setFilePath(TarFileHeader &header, const QByteArray &filePath) { if (filePath.length() <= int(sizeof header.fileName)) { std::memcpy(&header.fileName, filePath.data(), filePath.length()); return true; } int sepIndex = filePath.indexOf('/'); while (sepIndex != -1) { const int fileNamePart = filePath.length() - sepIndex; if (sepIndex <= int(sizeof header.fileNamePrefix) && fileNamePart <= int(sizeof header.fileName)) { std::memcpy(&header.fileNamePrefix, filePath.data(), sepIndex); std::memcpy(&header.fileName, filePath.data() + sepIndex + 1, fileNamePart); return true; } sepIndex = filePath.indexOf('/', sepIndex + 1); } return false; } static bool writeHeader(QFile &tarFile, const QFileInfo &fileInfo, const QString &remoteFilePath, const FilePath &tarFilePath, QString *errorMessage) { TarFileHeader header; std::memset(&header, '\0', sizeof header); if (!setFilePath(header, remoteFilePath.toUtf8())) { *errorMessage = Tr::tr("Cannot add file \"%1\" to tar-archive: path too long.") .arg(remoteFilePath); return false; } int permissions = (0400 * fileInfo.permission(QFile::ReadOwner)) | (0200 * fileInfo.permission(QFile::WriteOwner)) | (0100 * fileInfo.permission(QFile::ExeOwner)) | (040 * fileInfo.permission(QFile::ReadGroup)) | (020 * fileInfo.permission(QFile::WriteGroup)) | (010 * fileInfo.permission(QFile::ExeGroup)) | (04 * fileInfo.permission(QFile::ReadOther)) | (02 * fileInfo.permission(QFile::WriteOther)) | (01 * fileInfo.permission(QFile::ExeOther)); const QByteArray permissionString = QString::fromLatin1("%1").arg(permissions, sizeof header.fileMode - 1, 8, QLatin1Char('0')).toLatin1(); std::memcpy(&header.fileMode, permissionString.data(), permissionString.length()); const QByteArray uidString = QString::fromLatin1("%1").arg(fileInfo.ownerId(), sizeof header.uid - 1, 8, QLatin1Char('0')).toLatin1(); std::memcpy(&header.uid, uidString.data(), uidString.length()); const QByteArray gidString = QString::fromLatin1("%1").arg(fileInfo.groupId(), sizeof header.gid - 1, 8, QLatin1Char('0')).toLatin1(); std::memcpy(&header.gid, gidString.data(), gidString.length()); const QByteArray sizeString = QString::fromLatin1("%1").arg(fileInfo.size(), sizeof header.length - 1, 8, QLatin1Char('0')).toLatin1(); std::memcpy(&header.length, sizeString.data(), sizeString.length()); const QByteArray mtimeString = QString::fromLatin1("%1").arg( fileInfo.lastModified().toSecsSinceEpoch(), sizeof header.mtime - 1, 8, QLatin1Char('0')).toLatin1(); std::memcpy(&header.mtime, mtimeString.data(), mtimeString.length()); if (fileInfo.isDir()) header.typeflag = '5'; std::memcpy(&header.magic, "ustar", sizeof "ustar"); std::memcpy(&header.version, "00", 2); const QByteArray &owner = fileInfo.owner().toUtf8(); std::memcpy(&header.uname, owner.data(), qMin(owner.length(), sizeof header.uname - 1)); const QByteArray &group = fileInfo.group().toUtf8(); std::memcpy(&header.gname, group.data(), qMin(group.length(), sizeof header.gname - 1)); std::memset(&header.chksum, ' ', sizeof header.chksum); quint64 checksum = 0; for (size_t i = 0; i < sizeof header; ++i) checksum += reinterpret_cast(&header)[i]; const QByteArray checksumString = QString::fromLatin1("%1").arg(checksum, sizeof header.chksum - 1, 8, QLatin1Char('0')).toLatin1(); std::memcpy(&header.chksum, checksumString.data(), checksumString.length()); header.chksum[sizeof header.chksum-1] = 0; if (!tarFile.write(reinterpret_cast(&header), sizeof header)) { *errorMessage = Tr::tr("Error writing tar file \"%1\": %2") .arg(tarFilePath.toUserOutput(), tarFile.errorString()); return false; } return true; } bool TarPackageCreationStep::appendFile(QPromise &promise, QFile &tarFile, const QFileInfo &fileInfo, const QString &remoteFilePath, const FilePath &tarFilePath, bool ignoreMissingFiles) { QString errorMessage; if (!writeHeader(tarFile, fileInfo, remoteFilePath, tarFilePath, &errorMessage)) { raiseError(errorMessage); return false; } if (fileInfo.isDir()) { const QDir dir(fileInfo.absoluteFilePath()); const QStringList files = dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); for (const QString &fileName : files) { const QString thisLocalFilePath = dir.path() + QLatin1Char('/') + fileName; const QString thisRemoteFilePath = remoteFilePath + QLatin1Char('/') + fileName; if (!appendFile(promise, tarFile, QFileInfo(thisLocalFilePath), thisRemoteFilePath, tarFilePath, ignoreMissingFiles)) { return false; } } return true; } const QString nativePath = QDir::toNativeSeparators(fileInfo.filePath()); QFile file(fileInfo.filePath()); if (!file.open(QIODevice::ReadOnly)) { const QString message = Tr::tr("Error reading file \"%1\": %2.") .arg(nativePath, file.errorString()); if (ignoreMissingFiles) { raiseWarning(message); return true; } else { raiseError(message); return false; } } const int chunkSize = 1024 * 1024; emit addOutput(Tr::tr("Adding file \"%1\" to tarball...").arg(nativePath), OutputFormat::NormalMessage); // TODO: Wasteful. Work with fixed-size buffer. while (!file.atEnd() && file.error() == QFile::NoError && tarFile.error() == QFile::NoError) { const QByteArray data = file.read(chunkSize); tarFile.write(data); if (promise.isCanceled()) return false; } if (file.error() != QFile::NoError) { raiseError(Tr::tr("Error reading file \"%1\": %2.").arg(nativePath, file.errorString())); return false; } const int blockModulo = file.size() % TarBlockSize; if (blockModulo != 0) tarFile.write(QByteArray(TarBlockSize - blockModulo, 0)); if (tarFile.error() != QFile::NoError) { raiseError(Tr::tr("Error writing tar file \"%1\": %2.") .arg(QDir::toNativeSeparators(tarFile.fileName()), tarFile.errorString())); return false; } return true; } TarPackageCreationStepFactory::TarPackageCreationStepFactory() { registerStep(Constants::TarPackageCreationStepId); setDisplayName(Tr::tr("Create tarball")); setSupportedConfiguration(RemoteLinux::Constants::DeployToGenericLinux); setSupportedStepList(ProjectExplorer::Constants::BUILDSTEPS_DEPLOY); } } // RemoteLinux::Internal