[add]添加文件同步服务&&安装脚本添加自定义文件服务安装

This commit is contained in:
liang-ys 2026-04-28 15:38:17 +08:00
parent 79f03f12c2
commit cd5f931fb6
11 changed files with 10067 additions and 49 deletions

View File

@ -539,55 +539,9 @@ chmod 644 /usr/share/applications/${app_name}.desktop
update-desktop-database update-desktop-database
#为所有用户创建应用菜单快捷方式 end #为所有用户创建应用菜单快捷方式 end
#注册 sys_data_sync_server 到 systemd
#if [ "oe2203_aarch64" = "$OS_DEFINE" ]; then
SERVICE_NAME="sys_data_sync_server"
EXEC_PATH="$(dirname "$script_path")/product/$BIN_DIR_VER/sys_data_sync_server"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
if [ ! -f "$EXEC_PATH" ]; then
echo "ERROR: 未找到可执行文件 $EXEC_PATH"
else
echo "INFO: 开始注册 ${SERVICE_NAME} 到 systemd..."
chmod +x "$EXEC_PATH"
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=Sys Data Sync Server
After=network.target
[Service]
Type=simple
ExecStart=$EXEC_PATH
WorkingDirectory=$(dirname "$EXEC_PATH")
Restart=always
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
# 重新加载 systemd
systemctl daemon-reload
# 设置开机自启动
systemctl enable ${SERVICE_NAME}.service
# 启动服务
systemctl start ${SERVICE_NAME}.service
# 检查状态
if systemctl is-active --quiet "${SERVICE_NAME}"; then
echo "INFO:${SERVICE_NAME} 服务已成功启动并设置为开机自启"
else
echo "ERROR:${SERVICE_NAME} 启动失败,请执行:"
echo "journalctl -u ${SERVICE_NAME} -f"
fi
fi
#fi
#注册 sys_data_sync_server 服务
bash $(dirname "$script_path")/installer/others/sys_file_service_manager.sh install "$BIN_DIR_VER"
if [ "oe2203_aarch64" = "$OS_DEFINE" ]; then if [ "oe2203_aarch64" = "$OS_DEFINE" ]; then

View File

@ -0,0 +1,96 @@
#!/bin/bash
set -e
#注册 sys_file_service 到 systemd
SERVICE_NAME="sys_file_service"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
script_path="/opt/EnergyHub/installer"
install_service() {
echo "INFO开始注册 ${SERVICE_NAME} 到 systemd..."
EXEC_PATH="$(dirname "$script_path")/product/$BIN_DIR_VER/sys_file_service"
WORK_DIR="$(dirname "$EXEC_PATH")"
if [ ! -f "$EXEC_PATH" ]; then
echo "ERROR: 未找到可执行文件 ${EXEC_PATH}"
exit 1
fi
chmod +x "$EXEC_PATH"
cat > "$SERVICE_FILE" <<EOL
[Unit]
Description=Sys Data Sync Server Service
After=network.target
[Service]
Type=simple
ExecStart=$EXEC_PATH
WorkingDirectory=$WORK_DIR
Restart=always
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOL
systemctl daemon-reload
systemctl enable ${SERVICE_NAME}.service
systemctl restart ${SERVICE_NAME}.service
if systemctl is-active --quiet ${SERVICE_NAME}.service; then
echo "INFO${SERVICE_NAME} 服务已经成功启动并设置为开机自启!"
else
echo "ERROR: ${SERVICE_NAME} 服务启动失败!查看日志请执行:"
echo "journalctl -u ${SERVICE_NAME}.service -f"
fi
}
uninstall_service() {
echo "INFO正在卸载 ${SERVICE_NAME} 服务..."
if systemctl is-active --quiet ${SERVICE_NAME}.service; then
systemctl stop ${SERVICE_NAME}.service
fi
systemctl disable ${SERVICE_NAME}.service || true
if [ -f "$SERVICE_FILE" ]; then
rm -f "$SERVICE_FILE"
systemctl daemon-reload
echo "INFO${SERVICE_NAME} 服务已成功卸载!"
else
echo "WARNING: ${SERVICE_NAME} 服务文件未找到,可能已经被删除。"
fi
}
if [ $# -lt 1 ]; then
echo "用法:$0 {install|uninstall} [BIN_DIR_VER]"
exit 1
fi
ACTION="$1"
BIN_DIR_VER="$2"
# install 时 BIN_DIR_VER 必须提供
if [ "$ACTION" = "install" ] && [ -z "$BIN_DIR_VER" ]; then
echo "ERROR: 安装时必须提供 BIN_DIR_VER 参数!"
echo "用法:$0 install BIN_DIR_VER"
exit 1
fi
case "$ACTION" in
install)
install_service
;;
uninstall)
uninstall_service
;;
*)
echo "用法:$0 {install|uninstall} [BIN_DIR_VER]"
exit 1
;;
esac

View File

@ -9,7 +9,7 @@ SUBDIRS += \
sys_startup \ sys_startup \
sys_svn_file_sync_api \ sys_svn_file_sync_api \
sys_svn_file_sync \ sys_svn_file_sync \
sys_data_sync_server sys_file_service
sys_file_sync.depends = sys_file_sync_api sys_file_sync.depends = sys_file_sync_api
sys_svn_file_sync.depends = sys_svn_file_sync_api sys_svn_file_sync.depends = sys_svn_file_sync_api

View File

@ -0,0 +1,33 @@
#ifndef APPCOMPONENT_HPP
#define APPCOMPONENT_HPP
#include "oatpp/web/server/HttpConnectionHandler.hpp"
#include "oatpp/network/tcp/server/ConnectionProvider.hpp"
#include "oatpp/parser/json/mapping/ObjectMapper.hpp"
#include "oatpp/web/server/HttpRouter.hpp"
#include "oatpp/core/macro/component.hpp"
class AppComponent
{
public:
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::ServerConnectionProvider>, serverConnectionProvider)([]{
return oatpp::network::tcp::server::ConnectionProvider::createShared({"0.0.0.0", 8080, oatpp::network::Address::IP_4});
}());
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::web::server::HttpRouter>, httpRouter)([]{
return oatpp::web::server::HttpRouter::createShared();
}());
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::ConnectionHandler>, serverConnectionHandler)([]{
OATPP_COMPONENT(std::shared_ptr<oatpp::web::server::HttpRouter>, router);
return oatpp::web::server::HttpConnectionHandler::createShared(router);
}());
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::parser::json::mapping::ObjectMapper>, apiObjectMapper)([]{
return oatpp::parser::json::mapping::ObjectMapper::createShared();
}());
};
#endif // APPCOMPONENT_HPP

View File

@ -0,0 +1,321 @@
#ifndef FILECONTROLLER_HPP
#define FILECONTROLLER_HPP
#include "oatpp/web/server/api/ApiController.hpp"
#include "oatpp/web/mime/multipart/Reader.hpp"
#include "oatpp/web/mime/multipart/PartReader.hpp"
#include "oatpp/web/mime/multipart/PartList.hpp"
#include "oatpp/parser/json/mapping/ObjectMapper.hpp"
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/core/macro/component.hpp"
#include "oatpp/core/data/stream/FileStream.hpp"
#include "oatpp/web/mime/multipart/TemporaryFileProvider.hpp"
#include "oatpp/web/mime/multipart/InMemoryDataProvider.hpp"
#include "oatpp/core/data/resource/TemporaryFile.hpp"
#include "oatpp/core/utils/ConversionUtils.hpp"
#include "oatpp/web/protocol/http/outgoing/BufferBody.hpp"
#include "oatpp/web/protocol/http/outgoing/StreamingBody.hpp"
#include "oatpp/network/Url.hpp"
#include "miniz.h"
#include <QDir>
#include <QFile>
#include <QDirIterator>
#include <QDebug>
#include <QDateTime>
#include <QUrl>
#include <QObject>
#include OATPP_CODEGEN_BEGIN(ApiController)
class FileController : public oatpp::web::server::api::ApiController
{
public:
FileController(OATPP_COMPONENT(std::shared_ptr<oatpp::parser::json::mapping::ObjectMapper>, objectMapper))
: oatpp::web::server::api::ApiController(objectMapper){}
private:
static std::string ensureDir(const std::string& dir) {
const QString qdir = QString::fromUtf8(dir.c_str());
if (QDir().mkpath(qdir)) return dir;
throw std::runtime_error("create_directories failed");
}
static std::string joinPath(const std::string& a, const std::string& b) {
const QString qa = QString::fromUtf8(a.c_str());
const QString qb = QString::fromUtf8(b.c_str());
return QDir(qa).filePath(qb).toUtf8().constData();
}
static std::string sanitizeFileName(const oatpp::String& in) {
if (!in || in->empty()) return "upload.bin";
const QString raw = QString::fromUtf8(in->c_str());
QString name = QFileInfo(raw).fileName();
if (name.isEmpty() || name == "." || name =="..") return "upload.bin";
static const QString illegal = "\\/:*?\"<>|";
for (int i = 0; i < name.size(); ++i) {
if (illegal.contains(name.at(i))) {
name[i] = QChar('_');
}
}
return name.toUtf8().constData();
}
static bool endsWithZip(const std::string& path) {
return QString::fromUtf8(path.c_str()).endsWith(".zip", Qt::CaseInsensitive);
}
static QString zipExtractDirFor(const std::string& zipPath, const std::string& baseDir) {
const QString qZip = QString::fromUtf8(zipPath.c_str());
const QString qBase = QString::fromUtf8(baseDir.c_str());
const QString stem = QFileInfo(qZip).completeBaseName();
return QDir(qBase).filePath(stem);
}
static std::string pathStem(const std::string& file) {
return QFileInfo(QString::fromUtf8(file.c_str())).completeBaseName().toUtf8().constData();
}
static bool extractZip(const std::string& zipPath, const std::string& targetDir, std::string& outError) {
mz_zip_archive zip_archive;
memset(&zip_archive, 0, sizeof(zip_archive));
if (!mz_zip_reader_init_file(&zip_archive, zipPath.c_str(), 0)) {
outError = "Could not initialize zip reader";
return false;
}
mz_uint num_files = mz_zip_reader_get_num_files(&zip_archive);
for (mz_uint i = 0; i < num_files; i++) {
mz_zip_archive_file_stat file_stat;
if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) {
continue;
}
// 跳过目录条目miniz 会在解压文件时自动处理路径)
if (mz_zip_reader_is_file_a_directory(&zip_archive, i)) {
continue;
}
// 防御 Zip Slip 攻击:检查文件名是否包含 ".."
QString fileName = QString::fromUtf8(file_stat.m_filename);
if (fileName.contains("..")) continue;
// 构建完整输出路径
std::string fullPath = joinPath(targetDir, file_stat.m_filename);
// 确保子目录存在
QFileInfo fi(QString::fromUtf8(fullPath.c_str()));
QDir().mkpath(fi.absolutePath());
// 解压文件
if (!mz_zip_reader_extract_to_file(&zip_archive, i, fullPath.c_str(), 0)) {
outError = "Failed to extract: " + std::string(file_stat.m_filename);
mz_zip_reader_end(&zip_archive);
return false;
}
}
mz_zip_reader_end(&zip_archive);
return true;
}
static bool zipDirectoryToFile(const std::string& srcDir, const std::string& zipFile, std::string* err) {
mz_zip_archive zip;
std::memset(&zip, 0, sizeof(zip));
if(!mz_zip_writer_init_file(&zip, zipFile.c_str(), 0)) {
if(err) *err = "create zip failed";
return false;
}
const QString root = QDir(QString::fromUtf8(srcDir.c_str())).absolutePath();
QDirIterator it(root, QDir::Files, QDirIterator::Subdirectories);
while(it.hasNext()) {
const QString abs = it.next();
const QString rel = QDir(root).relativeFilePath(abs);
const std::string zipEntry = QDir::fromNativeSeparators(rel).toUtf8().constData();
if(!mz_zip_writer_add_file(&zip,
zipEntry.c_str(),
abs.toUtf8().constData(),
NULL,
0,
MZ_BEST_COMPRESSION)) {
if(err) *err = "zip add failed: " + zipEntry;
mz_zip_writer_end(&zip);
return false;
}
}
if(!mz_zip_writer_finalize_archive(&zip)) {
if(err) *err = "zip finalize failed";
mz_zip_writer_end(&zip);
return false;
}
mz_zip_writer_end(&zip);
return true;
}
static void cleanupOldTempZips(const std::string& dir, qint64 olderThanSeconds) {
QDir qdir(QString::fromUtf8(dir.c_str()));
if(!qdir.exists()) return;
const QDateTime now = QDateTime::currentDateTimeUtc();
const QFileInfoList list = qdir.entryInfoList(QStringList() << "download_*.zip", QDir::Files, QDir::Time);
for(int i = 0; i < list.size(); ++i) {
const QFileInfo fi = list.at(i);
const qint64 age = fi.lastModified().toUTC().secsTo(now);
if(age > olderThanSeconds) {
QFile::remove(fi.absoluteFilePath());
}
}
}
static std::string urlDecodeToStdString(const oatpp::String& encoded) {
if (!encoded) {
return {};
}
const std::string raw = *encoded;
QByteArray bytes(raw.data(), static_cast<int>(raw.size()));
QString decoded = QUrl::fromPercentEncoding(bytes);
QByteArray utf8 = decoded.toUtf8();
return std::string(utf8.constData(), static_cast<size_t>(utf8.size()));
}
public:
ENDPOINT("GET", "/hello", hello)
{
auto response = createResponse(Status::CODE_200, u8"hello");
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
ENDPOINT("POST", "/upload", upload,
REQUEST(std::shared_ptr<IncomingRequest>, request),
QUERY(oatpp::String, dir, "dir", "uploads"),
QUERY(oatpp::String, field, "field", "file"))
{
auto contentType = request->getHeader("Content-Type");
if (!contentType || contentType->find("multipart/form-data") == std::string::npos) {
auto response = createResponse(Status::CODE_400, u8"Invalid Content-Type");
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
const std::string baseDir = ensureDir(dir ? urlDecodeToStdString(dir) : std::string("uploads"));
const std::string tmpDir = ensureDir(joinPath(baseDir, ".tmp"));
auto multipart = std::make_shared<oatpp::web::mime::multipart::PartList>(request->getHeaders());
oatpp::web::mime::multipart::Reader reader(multipart.get());
const auto fieldName = std::string(field ? *field : "file");
// 只把目标字段写入临时文件;其余字段限定在小内存中解析。
reader.setPartReader(fieldName.c_str(), oatpp::web::mime::multipart::createTemporaryFilePartReader(tmpDir.c_str()));
reader.setDefaultPartReader(oatpp::web::mime::multipart::createInMemoryPartReader(16 * 1024));
request->transferBody(&reader);
auto filePart = multipart->getNamedPart(oatpp::String(fieldName.c_str()));
if(!filePart) {
auto response = createResponse(Status::CODE_400, u8"未找到文件部分 (field=" + (field ? std::string(*field) : std::string("file")) + ")");
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
auto filename = sanitizeFileName(filePart->getFilename());
const std::string outPath = joinPath(baseDir, filename);
auto tf = std::dynamic_pointer_cast<oatpp::data::resource::TemporaryFile>(filePart->getPayload());
if (!tf) {
auto response = createResponse(Status::CODE_400, u8"没有临时文件数据包");
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
if (!tf->moveFile(oatpp::String(outPath.c_str()))) {
auto response = createResponse(Status::CODE_500, u8"保存失败: TemporaryFile::moveFile 函数执行失败");
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
if (endsWithZip(outPath)) {
const QString qDestDir = zipExtractDirFor(outPath, baseDir);
std::string destDir = qDestDir.toUtf8().constData();
std::string errStr;
if (extractZip(outPath, destDir, errStr)) {
QFile::remove(QString::fromUtf8(outPath.c_str()));
auto response = createResponse(Status::CODE_200, u8"压缩文件接收并提取至: " + destDir);
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
else {
auto response = createResponse(Status::CODE_500, u8"压缩文件提取失败:" + errStr);
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
}
auto response = createResponse(Status::CODE_200, u8"接收并保存至:" + outPath);
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
ENDPOINT("GET", "/download", download,
QUERY(oatpp::String, dir, "dir"))
{
if (!dir || dir->empty()) {
auto response = createResponse(Status::CODE_400, u8"缺少dir字段");
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
const std::string sourceDir = urlDecodeToStdString(dir);
QFileInfo info(QString::fromUtf8(sourceDir.c_str()));
if (!info.exists() || !info.isDir()) {
auto response = createResponse(Status::CODE_404, u8"未找到该目录: " + sourceDir);
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
const std::string tmpRoot = ensureDir(".tmp");
cleanupOldTempZips(tmpRoot, 3600);
const std::string zipName = "download_" + std::to_string(oatpp::base::Environment::getMicroTickCount()) + ".zip";
const std::string zipPath = joinPath(tmpRoot, zipName);
std::string err;
if(!zipDirectoryToFile(sourceDir, zipPath, &err)) {
QFile::remove(QString::fromUtf8(zipPath.c_str()));
auto response = createResponse(Status::CODE_500, u8"创建压缩文件失败: " + err);
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
const std::string downloadName = pathStem(sanitizeFileName(oatpp::String(sourceDir.c_str()))) + ".zip";
auto fileStream = std::make_shared<oatpp::data::stream::FileInputStream>(zipPath.c_str());
if(fileStream->getFile() == nullptr) {
QFile::remove(QString::fromUtf8(zipPath.c_str()));
auto response = createResponse(Status::CODE_500, u8"打开压缩文件并流传输失败");
response->putHeader("Content-Type", "text/plain; charset=utf-8");
return response;
}
auto streamingBody =
std::make_shared<oatpp::web::protocol::http::outgoing::StreamingBody>(fileStream);
auto response =
oatpp::web::protocol::http::outgoing::Response::createShared(Status::CODE_200, streamingBody);
response->putHeader("Content-Type", "application/zip");
response->putHeader("Content-Disposition", "attachment; filename=\"" + downloadName + "\"");
return response;
}
};
#include OATPP_CODEGEN_END(ApiController)
#endif // FILECONTROLLER_HPP

View File

@ -0,0 +1,34 @@
#include <QCoreApplication>
#include <QDebug>
#include "oatpp/core/base/Environment.hpp"
#include "oatpp/network/Server.hpp"
#include "AppComponent.hpp"
#include "FileController.hpp"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
oatpp::base::Environment::init();
qDebug() << "Oat++ Version:" << OATPP_VERSION;
qDebug() << "Oat++ is running with Qt!";
AppComponent components;
OATPP_COMPONENT(std::shared_ptr<oatpp::web::server::HttpRouter>, router);
OATPP_COMPONENT(std::shared_ptr<oatpp::parser::json::mapping::ObjectMapper>, mapper);
router->addController(std::make_shared<FileController>(mapper));
OATPP_COMPONENT(std::shared_ptr<oatpp::network::ConnectionHandler>, connectionHandler);
OATPP_COMPONENT(std::shared_ptr<oatpp::network::ServerConnectionProvider>, connectionProvider);
oatpp::network::Server server(connectionProvider, connectionHandler);
server.run();
oatpp::base::Environment::destroy();
return a.exec();
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
@echo off
setlocal
REM 固定用户名
set username=admin
echo 请输入远程主机IP地址
set /p ip=
if "%ip%"=="" (
echo IP地址不能为空
pause
exit
)
echo.
echo 请选择操作:
echo 1 - 启动系统
echo 2 - 停止系统
set /p choice=请输入 1 或 2
if "%choice%"=="1" (
set command=/opt/EnergyHub/platform/oe2203_aarch64_release/sys_ctrl
) else if "%choice%"=="2" (
set command=/opt/EnergyHub/platform/oe2203_aarch64_release/sys_ctrl -s
) else (
echo 无效选择!
pause
exit
)
echo.
echo 正在连接 %ip% ...
echo 如果需要密码,请根据提示输入。
echo.
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL %username%@%ip% %command%
echo.
echo 执行完成。
pause

View File

@ -0,0 +1,64 @@
#!/bin/bash
# 终端检测与自动弹出
if [ ! -t 0 ]; then
# 读取系统发行版本信息
if [ -f /etc/os-release ]; then
source /etc/os-release
# 麒麟使用 mate-terminal其他的使用 gnome-terminal
if [[ "$ID" == "kylin" ]]; then
TERM_CMD="mate-terminal"
else
TERM_CMD="gnome-terminal"
fi
# 执行命令并退出当前无窗口进程
if command -v "$TERM_CMD" > /dev/null 2>&1; then
exec "$TERM_CMD" -- bash "$0" "$@"
else
# 最后的兜底,防止某些精简版系统没装对应的终端
xterm -e bash "$0" "$@" || echo "找不到合适的终端程序"
fi
fi
exit 0
fi
# 固定用户名
username="admin"
echo "请输入远程主机IP地址"
read ip
if [ -z "$ip" ]; then
echo "IP地址不能为空"
read -p "按回车键退出..."
exit 1
fi
echo
echo "请选择操作:"
echo "1 - 启动系统"
echo "2 - 停止系统"
read -p "请输入 1 或 2" choice
if [ "$choice" = "1" ]; then
command="/opt/EnergyHub/platform/oe2203_aarch64_release/sys_ctrl"
elif [ "$choice" = "2" ]; then
command="/opt/EnergyHub/platform/oe2203_aarch64_release/sys_ctrl -s"
else
echo "无效选择!"
read -p "按回车键退出..."
exit 1
fi
echo
echo "正在连接 $ip ..."
echo "如果需要密码,请根据提示输入。"
echo
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${username}@${ip} "$command"
echo
echo "执行完成。"
read -p "按回车键退出..."

View File

@ -0,0 +1,56 @@
QT -= gui
CONFIG += c++11 console
CONFIG -= app_bundle
# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += main.cpp \
miniz.c
HEADERS += \
AppComponent.hpp \
miniz.h \
FileController.hpp
LIBS += -loatpp
COMMON_PRI=$$PWD/../../common.pri
exists($$COMMON_PRI) {
include($$COMMON_PRI)
}else {
error("FATAL error: can not find common.pri")
}
# 脚本目录
SCRIPT_DIR = $$PWD/scripts
win32 {
SCRIPT_SRC = $$shell_path($$SCRIPT_DIR/remote_sys_ctrl.bat)
SCRIPT_DST = $$shell_path($$DESTDIR/remote_sys_ctrl.bat)
generate_script.commands = $$QMAKE_COPY "$$SCRIPT_SRC" "$$SCRIPT_DST"
}
unix:!macx { #Linux
SCRIPT_SRC = $$SCRIPT_DIR/remote_sys_ctrl.sh
SCRIPT_DST = $$DESTDIR/remote_sys_ctrl.sh
generate_script.commands = cp "$$SCRIPT_SRC" "$$SCRIPT_DST" && chmod +x "$$SCRIPT_DST"
}
QMAKE_EXTRA_TARGETS += generate_script
POST_TARGETDEPS += generate_script
# ------------------------------------------------------------------