임베디드 관련 카테고리/Qt

Qt Installer 자동화 도구 구현하기: 디렉터리 구조 생성부터 압축까지

CBJH 2024. 11. 8.
728x90
반응형

Qt 기반으로 개발한 프로그램을 손쉽게 설치할 수 있도록, 디렉터리 구조 생성 및 압축 과정을 자동화하는 방법에 대해 알아보겠습니다. 이 글에서는 Qt Creator를 사용하여 특정 디렉터리 구조를 생성하고, 선택한 폴더를 압축한 후 지정된 위치에 저장하는 코드를 구현하는 방법을 설명합니다.

참고: 코드에는 특정 사용자 정보나 프로그램 이름 대신 일반적인 용어로 수정되었습니다.

 

관련 링크


1. mainwindow.ui를 먼저 구현해야됩니다.

참고용 이미지입니다.

UI 구성 요소

  1. Program Name (프로그램 이름) 입력란
    • 위치: 메인 창 상단
    • 이름: programNameInput
    • 설명: 사용자가 설치하려는 프로그램의 이름을 입력하는 텍스트 입력란입니다.
    • 기능: 입력된 프로그램 이름을 기반으로 설치 경로가 자동 업데이트됩니다.
  2. Version (버전) 입력란
    • 위치: Program Name 입력란 아래
    • 이름: versionInput
    • 설명: 프로그램 버전을 입력하는 텍스트 입력란입니다.
    • 기능: 입력된 버전을 기반으로 설치 경로가 자동 업데이트됩니다.
  3. Install Path (설치 경로) 표시란
    • 위치: Version 입력란 아래
    • 이름: installPathInput
    • 설명: 자동 생성된 설치 경로를 표시하는 비활성 텍스트 입력란입니다.
    • 기능: 사용자가 입력한 프로그램 이름과 버전 정보를 기반으로 설치 경로를 실시간으로 업데이트하여 표시합니다.
  4. Select Folder (폴더 선택) 버튼
    • 위치: 설치 경로 표시란 아래
    • 이름: selectFileButton
    • 설명: 사용자가 압축할 폴더를 선택할 수 있는 버튼입니다.
    • 기능: 버튼을 클릭하면 파일 선택 대화 상자가 열리고, 사용자는 압축할 폴더를 선택할 수 있습니다.
  5. Save (저장) 버튼
    • 위치: 폴더 선택 버튼 아래
    • 이름: saveButton
    • 설명: 디렉터리 구조를 생성하고 XML 파일을 생성하며 선택한 폴더를 압축하여 저장하는 버튼입니다.
    • 기능: 버튼을 클릭하면 compressSelectedFile()와 saveVersionToXml() 함수가 호출되어 전체 설치 파일 생성 과정이 실행됩니다.
  6. Result (결과) 레이블
    • 위치: 메인 창 하단
    • 이름: resultLabel
    • 설명: 디렉터리 및 파일 생성, 압축 결과를 출력하는 레이블입니다.
    • 기능: 실행 후 성공 또는 실패 메시지를 표시합니다.
  7. File Path (선택된 파일 경로) 레이블
    • 위치: Select Folder 버튼 옆 또는 아래
    • 이름: filePathLabel
    • 설명: 사용자가 선택한 폴더의 경로를 표시하는 레이블입니다.
    • 기능: Select Folder 버튼을 통해 폴더를 선택하면 선택된 폴더의 경로를 표시합니다.

2. InstallerFileManager 클래스 개요

이 코드는 InstallerFileManager 클래스를 사용하여 디렉터리 구조를 만들고 XML 파일 및 라이선스 파일을 생성합니다. 또한, 선택한 릴리스 폴더를 ZIP 파일로 압축하여 저장할 수 있습니다.

#include <QFile>
#include <QDir>
#include <QMessageBox>
#include <QDate>
#include <QProcess>
#include <QDebug>
#include <QCoreApplication>

이 클래스는 programName, version, installPath 세 가지 변수를 기반으로 생성됩니다. 각각의 변수는 프로그램 이름, 버전, 그리고 설치 경로를 지정합니다.

디렉터리 구조 생성 함수

createDirectoryStructure() 함수는 설치 파일들이 저장될 폴더 구조를 자동으로 생성합니다.

 

* 프로젝트 구성예시 및 관련 글 : https://cbjh-4.tistory.com/260

bool InstallerFileManager::createDirectoryStructure() {
    QDir dir;
    QStringList paths = {
        installPath + "/config",
        installPath + "/packages",
        installPath + "/packages/com.generic." + programName.toLower().replace(" ", "") + "/meta",
        installPath + "/packages/com.generic." + programName.toLower().replace(" ", "") + "/data",
        installPath + "/repository"
    };

    for (const QString& path : paths) {
        if (!dir.mkpath(path)) {
            QMessageBox::warning(nullptr, "Error", QString("Failed to create directory: %1").arg(path));
            return false;
        }

        // 각 디렉터리에 __init__.py 파일 생성
        QFile initFile(path + "/__init__.py");
        if (!initFile.open(QIODevice::WriteOnly)) {
            QMessageBox::warning(nullptr, "Error", QString("Failed to create __init__.py in: %1").arg(path));
            return false;
        }
        initFile.close();
    }

    return true;
}

paths 리스트에는 필요한 디렉터리 구조가 포함되어 있으며, 각각의 디렉터리에 __init__.py 파일이 생성됩니다.


config.xml 및 package.xml 생성 함수

설치에 필요한 config.xmlpackage.xml을 자동으로 생성하는 함수들입니다.

createConfigXml() 함수는 config.xml을 생성하며, programNameversion에 맞는 내용을 자동으로 작성합니다.

bool InstallerFileManager::createConfigXml() {
    // XML 작성 시작
    QDomDocument doc;
    QDomElement root = doc.createElement("Installer");
    doc.appendChild(root);

    // 기본 정보 추가
    root.appendChild(createElement(doc, "Name", programName));
    root.appendChild(createElement(doc, "Version", version));
    root.appendChild(createElement(doc, "Title", programName + " Installer"));
    root.appendChild(createElement(doc, "Publisher", "GenericCompany"));

    // Repository URL 설정
    QDomElement repositories = doc.createElement("RemoteRepositories");
    QDomElement repository = doc.createElement("Repository");
    repository.appendChild(createElement(doc, "Url", QString("https://example.com/%1-installer/v%2/repository")
                                          .arg(programName.toLower()).arg(version)));
    root.appendChild(repositories);

    return saveXmlToFile(doc, installPath + "/config/config.xml");
}

3. 압축 함수: 선택한 폴더를 ZIP으로 압축하기

압축 과정은 PowerShell의 Compress-Archive 명령어를 이용해 지정한 폴더를 압축하고 최상위 폴더 없이 하위 파일들만 포함하도록 설정합니다.

bool InstallerFileManager::compressFileToZip(const QString& folderPath) {
    QString exeDir = QCoreApplication::applicationDirPath();
    QString tempArchivePath = exeDir + "/" + QFileInfo(folderPath).fileName() + ".zip";

    QFile::remove(tempArchivePath);  // 기존 압축 파일 삭제

    QProcess process;
    QStringList arguments;
    QStringList filePaths;
    QDir dir(folderPath);
    dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 

    QFileInfoList files = dir.entryInfoList();
    for (const QFileInfo& fileInfo : files) {
        filePaths << fileInfo.absoluteFilePath();
    }

    // PowerShell 명령어 생성
    arguments << "-Command"
              << QString("Compress-Archive -Path '%1' -DestinationPath '%2' -Force")
                     .arg(filePaths.join("','"))
                     .arg(QDir::toNativeSeparators(tempArchivePath));

    process.start("powershell", arguments);
    process.waitForFinished();
    QString errorOutput = process.readAllStandardError();

    if (process.exitCode() != 0) {
        QMessageBox::warning(nullptr, "Compression Error", "Failed to compress folder:\n" + errorOutput);
        return false;
    }

    return true;
}

4. MainWindow 클래스: UI와 기능 연결

MainWindow 클래스는 사용자가 선택한 프로그램 이름, 버전, 설치 경로를 기반으로 디렉터리 구조를 생성하고, 릴리스 폴더를 선택하여 압축을 수행합니다.

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    connect(ui->saveButton, &QPushButton::clicked, this, &MainWindow::handleSaveButtonClick);
    connect(ui->selectFileButton, &QPushButton::clicked, this, &MainWindow::selectFile);
    connect(ui->programNameInput, &QLineEdit::textChanged, this, &MainWindow::updateInstallPath);
    connect(ui->versionInput, &QLineEdit::textChanged, this, &MainWindow::updateInstallPath);

    updateInstallPath();
}

5. 사용 예시: 폴더 선택 후 압축하기

사용자는 UI에서 프로그램 이름, 버전, 설치 경로를 입력하고, Save 버튼을 클릭하여 디렉터리 구조를 생성할 수 있습니다. Select Folder 버튼을 통해 압축할 폴더를 선택한 후, 선택된 폴더의 하위 파일들을 ZIP으로 압축하여 지정된 위치에 저장합니다.

void MainWindow::compressSelectedFile() {
    if (selectedFilePath.isEmpty()) {
        QMessageBox::warning(this, "Error", "No folder selected for compression.");
        return;
    }

    if (fileManager && fileManager->compressFileToZip(selectedFilePath)) {
        ui->resultLabel->setText("Folder compressed successfully!");
    } else {
        ui->resultLabel->setText("Folder compression failed.");
    }
}

6. 전체 코드

InstallerFileManager 클래스

InstallerFileManager 클래스는 설치 디렉터리 구조를 생성하고, config.xmlpackage.xml 파일을 작성하며, 라이선스 파일을 생성하고 선택한 폴더를 ZIP 압축으로 저장하는 기능을 포함합니다.

#include "installerfilemanager.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 

// 클래스 생성자
InstallerFileManager::InstallerFileManager(const QString& programName, const QString& version, const QString& installPath)
    : programName(programName), version(version), installPath(installPath)
{
}

// 디렉터리 구조 생성 함수
bool InstallerFileManager::createDirectoryStructure() {
    QDir dir;
    QStringList paths = {
        installPath + "/config",
        installPath + "/packages",
        installPath + "/packages/com.generic." + programName.toLower().replace(" ", "") + "/meta",
        installPath + "/packages/com.generic." + programName.toLower().replace(" ", "") + "/data",
        installPath + "/repository"
    };

    for (const QString& path : paths) {
        if (!dir.mkpath(path)) {
            QMessageBox::warning(nullptr, "Error", QString("Failed to create directory: %1").arg(path));
            return false;
        }

        // 각 디렉터리에 __init__.py 파일 생성
        QFile initFile(path + "/__init__.py");
        if (!initFile.open(QIODevice::WriteOnly)) {
            QMessageBox::warning(nullptr, "Error", QString("Failed to create __init__.py in: %1").arg(path));
            return false;
        }
        initFile.close();
    }

    return true;
}

// config.xml 생성 함수
bool InstallerFileManager::createConfigXml() {
    QDomDocument doc;
    QDomElement root = doc.createElement("Installer");
    doc.appendChild(root);

    root.appendChild(createElement(doc, "Name", programName));
    root.appendChild(createElement(doc, "Version", version));
    root.appendChild(createElement(doc, "Title", programName + " Installer"));
    root.appendChild(createElement(doc, "Publisher", "GenericCompany"));
    root.appendChild(createElement(doc, "ProductUrl", "https://example.com/" + programName.toLower() + "-installer/"));
    root.appendChild(createElement(doc, "InstallerWindowIcon", "installericon.png"));
    root.appendChild(createElement(doc, "InstallerApplicationIcon", "installericon.png"));
    root.appendChild(createElement(doc, "Logo", "logo.png"));
    root.appendChild(createElement(doc, "TargetDir", QString("@DesktopDir@/%1_v%2").arg(programName).arg(version)));

    QDomElement repositories = doc.createElement("RemoteRepositories");
    QDomElement repository = doc.createElement("Repository");
    repository.appendChild(createElement(doc, "Url", QString("https://example.com/%1-installer/v%2/repository")
                                          .arg(programName.toLower()).arg(version)));
    repositories.appendChild(repository);
    root.appendChild(repositories);

    return saveXmlToFile(doc, installPath + "/config/config.xml");
}

// package.xml 생성 함수
bool InstallerFileManager::createPackageXml() {
    QDomDocument doc;
    QDomElement root = doc.createElement("Package");
    doc.appendChild(root);

    root.appendChild(createElement(doc, "DisplayName", programName + " (v" + version + ")"));
    root.appendChild(createElement(doc, "Description", "A simple " + programName + " game."));
    root.appendChild(createElement(doc, "Version", version));
    root.appendChild(createElement(doc, "ReleaseDate", QDate::currentDate().toString("yyyy-MM-dd")));
    root.appendChild(createElement(doc, "Name", "com.generic." + programName.toLower().replace(" ", "")));

    QDomElement licenses = doc.createElement("Licenses");
    QDomElement license = doc.createElement("License");
    license.setAttribute("name", "License Agreement");
    license.setAttribute("file", "license.txt");
    licenses.appendChild(license);
    root.appendChild(licenses);

    root.appendChild(createElement(doc, "DownloadableArchives", programName + "_v" + version + ".zip"));

    return saveXmlToFile(doc, installPath + "/packages/com.generic." + programName.toLower().replace(" ", "") + "/meta/package.xml");
}

// license.txt 생성 함수
bool InstallerFileManager::createLicenseFile() {
    QFile file(installPath + "/packages/com.generic." + programName.toLower().replace(" ", "") + "/meta/license.txt");
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QMessageBox::warning(nullptr, "Error", "Failed to create license.txt");
        return false;
    }

    QTextStream out(&file);
    out << "Generic Game License Agreement\n";
    out << "------------------------------------------\n\n";
    out << "This software was created by GenericCompany.\n\n";
    out << "Users are free to use, modify, and distribute this software under certain conditions.\n\n";

    file.close();
    return true;
}

// XML 요소 생성 함수
QDomElement InstallerFileManager::createElement(QDomDocument& doc, const QString& tag, const QString& value) {
    QDomElement element = doc.createElement(tag);
    element.appendChild(doc.createTextNode(value));
    return element;
}

// XML 파일 저장 함수
bool InstallerFileManager::saveXmlToFile(const QDomDocument& doc, const QString& filePath) {
    QFile file(filePath);
    if (!file.open(QIODevice::WriteOnly)) {
        QMessageBox::warning(nullptr, "Error", QString("Failed to create %1").arg(filePath));
        return false;
    }

    QTextStream stream(&file);
    stream << doc.toString();
    file.close();
    return true;
}

// 폴더를 ZIP으로 압축하는 함수
bool InstallerFileManager::compressFileToZip(const QString& folderPath) {
    QString exeDir = QCoreApplication::applicationDirPath();
    QString tempArchivePath = exeDir + "/" + QFileInfo(folderPath).fileName() + ".zip";
    QFile::remove(tempArchivePath);

    QProcess process;
    QStringList arguments;
    QStringList filePaths;
    QDir dir(folderPath);
    dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);

    QFileInfoList files = dir.entryInfoList();
    for (const QFileInfo& fileInfo : files) {
        filePaths << fileInfo.absoluteFilePath();
    }

    arguments << "-Command"
              << QString("Compress-Archive -Path '%1' -DestinationPath '%2' -Force")
                     .arg(filePaths.join("','"))
                     .arg(QDir::toNativeSeparators(tempArchivePath));

    process.start("powershell", arguments);
    process.waitForFinished();
    QString errorOutput = process.readAllStandardError();

    if (process.exitCode() != 0) {
        QMessageBox::warning(nullptr, "Compression Error", "Failed to compress folder:\n" + errorOutput);
        return false;
    }

    return true;
}

MainWindow 클래스

MainWindow 클래스는 UI와의 연동을 처리합니다. 사용자는 이 인터페이스에서 프로그램 이름, 버전, 설치 경로를 입력하고, 폴더를 선택하여 압축할 수 있습니다.

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "installerfilemanager.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QProcess>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    connect(ui->saveButton, &QPushButton::clicked, this, &MainWindow::handleSaveButtonClick);
    connect(ui->selectFileButton, &QPushButton::clicked, this, &MainWindow::selectFile);
    connect(ui->programNameInput, &QLineEdit::textChanged, this, &MainWindow::updateInstallPath);
    connect(ui->versionInput, &QLineEdit::textChanged, this, &MainWindow::updateInstallPath);

    updateInstallPath();
}

MainWindow::~MainWindow() {
    delete ui;
}

void MainWindow::handleSaveButtonClick() {
    compressSelectedFile();
    saveVersionToXml();
}

void MainWindow::updateInstallPath() {
    QString programName = ui->programNameInput->text().isEmpty() ? "ProgramName" : ui->programNameInput->text();
    QString version = ui->versionInput->text().isEmpty() ? "1.0.0" : ui->versionInput->text();
    QString defaultPath = QString("D:/qt/%1/v%2").arg(programName).arg(version);
    ui->installPathInput->setText(defaultPath);
}

void MainWindow::saveVersionToXml() {
    QString programName = ui->programNameInput->text();
    QString version = ui->versionInput->text();
    QString installPath = ui->installPathInput->text();
    fileManager = new InstallerFileManager(programName, version, installPath);

    if (!fileManager->createDirectoryStructure() ||
        !fileManager->createConfigXml() ||
        !fileManager->createPackageXml() ||
        !fileManager->createLicenseFile()) {
        ui->resultLabel->setText("Failed to create files or directory structure.");
        return;
    }

    QString tempArchivePath = QCoreApplication::applicationDirPath() + "/" + QFileInfo(selectedFilePath).fileName() + ".zip";
    QString finalArchivePath = installPath + "/packages/com.generic." + programName.toLower().replace(" ", "") + "/data/" + QFileInfo(selectedFilePath).fileName() + ".zip";

    if (QFile::exists(finalArchivePath)) {
        QFile::remove(finalArchivePath);
    }

    QFile::rename(tempArchivePath, finalArchivePath);
    ui->resultLabel->setText("Files and directory structure created and compressed file moved successfully!");
}

void MainWindow::selectFile() {
    selectedFilePath = QFileDialog::getExistingDirectory(this, "Select Folder to Compress");

    if (!selectedFilePath.isEmpty()) {
        ui->filePathLabel->setText(selectedFilePath);
    }
}

void MainWindow::compressSelectedFile() {
    if (selectedFilePath.isEmpty()) {
        QMessageBox::warning(this, "Error", "No folder selected for compression.");
        return;
    }

    if (fileManager && fileManager->compressFileToZip(selectedFilePath)) {
        ui->resultLabel->setText("Folder compressed successfully!");
    } else {
        ui->resultLabel->setText("Folder compression failed.");
    }
}

마무리

이 코드를 통해 설치 프로그램에 필요한 폴더 구조와 파일을 자동으로 생성하고, 선택한 폴더를 ZIP으로 압축하여 최종 설치 파일을 생성할 수 있습니다. 이를 통해 배포 파일을 보다 쉽게 준비할 수 있습니다.

댓글