v2026.10.01_002: 배포 폴더 구조 (맨 위 3개 파일 + Lib + Logs)
- 시작 프로그램 (windows/launcher, C 런타임 정적 연결): 맨 위 jj_mkvmaker.exe 가 Lib\jj_mkvmaker.exe 를 같은 인수로 실행 - tool/build_launcher.ps1, tool/make_layout.ps1 (배포 폴더 · 릴리스 zip 배치) - 작업 기록 · 종료 기록을 배포 폴더의 Logs 에 저장 (AppPaths) - 업데이트: 배포 폴더 맨 위에 설치, 압축 파일에서 가장 위의 jj_mkvmaker.exe 기준 - 예전 한 폴더 구조에서 넘어오면 맨 위에 남은 예전 프로그램 파일 정리 - 끌어다 놓기 테스트: PC 가 바쁠 때도 통과하도록 대기 시간 늘림 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main v2026.10.01_002
parent
08c5fcd2e9
commit
ebea6ddbb6
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
1. [Releases](../../releases) 에서 `JJ_MKVMaker_v*_win64.zip` 을 받아 원하는 폴더에 풉니다.
|
1. [Releases](../../releases) 에서 `JJ_MKVMaker_v*_win64.zip` 을 받아 원하는 폴더에 풉니다.
|
||||||
2. `jj_mkvmaker.exe` 를 실행합니다. (Windows 10/11 64비트)
|
2. `jj_mkvmaker.exe` 를 실행합니다. (Windows 10/11 64비트)
|
||||||
3. 폴더 전체가 프로그램입니다. exe 파일 하나만 옮기면 동작하지 않습니다.
|
3. 폴더 전체가 프로그램입니다. 맨 위의 `jj_mkvmaker.exe` 는 `Lib\` 안의 프로그램을 실행하는 시작 프로그램이고, 작업 기록은 `Logs\` 에 남습니다. exe 파일 하나만 옮기면 동작하지 않습니다.
|
||||||
|
|
||||||
처음 쓸 때 내려받는 것 (한 번만):
|
처음 쓸 때 내려받는 것 (한 번만):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'app/download_manager.dart';
|
||||||
import 'app/settings.dart';
|
import 'app/settings.dart';
|
||||||
import 'core/app_update.dart';
|
import 'core/app_update.dart';
|
||||||
import 'core/playlist.dart';
|
import 'core/playlist.dart';
|
||||||
|
import 'platform/windows/app_paths.dart';
|
||||||
import 'platform/windows/com_guard.dart';
|
import 'platform/windows/com_guard.dart';
|
||||||
import 'platform/windows/desktop_shell.dart';
|
import 'platform/windows/desktop_shell.dart';
|
||||||
import 'platform/windows/exit_trace.dart';
|
import 'platform/windows/exit_trace.dart';
|
||||||
|
|
@ -59,16 +60,22 @@ Future<void> main(List<String> args) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
final services = PlatformServices.create();
|
final services = PlatformServices.create();
|
||||||
|
// 작업 기록: 배포 폴더의 Logs (쓸 수 없으면 설정 폴더 아래 Logs)
|
||||||
|
final logsDir = AppPaths.logsDir(dataDir);
|
||||||
final controller = AppController(services, settingsStore: SettingsStore())
|
final controller = AppController(services, settingsStore: SettingsStore())
|
||||||
..logFile = p.join(dataDir, 'app.log');
|
..logFile = p.join(logsDir, 'app.log');
|
||||||
|
ExitTrace.file = p.join(logsDir, 'exit.log');
|
||||||
// 지난 실행이 정상으로 끝났는지 (아니면 알려 주고 기록에 남긴다)
|
// 지난 실행이 정상으로 끝났는지 (아니면 알려 주고 기록에 남긴다)
|
||||||
final session = SessionMarker(p.join(dataDir, 'session.json'));
|
final session = SessionMarker(p.join(dataDir, 'session.json'));
|
||||||
final crashed = session.start();
|
final crashed = session.start();
|
||||||
controller.note('── 시작 $appTitle ──');
|
controller.note('── 시작 $appTitle (${AppPaths.exeDir}) ──');
|
||||||
|
// 예전 구조 (한 폴더) 에서 Lib 구조로 바뀐 뒤 맨 위에 남은 예전 프로그램 파일 정리
|
||||||
|
final cleaned = AppPaths.cleanupOldLayout();
|
||||||
|
if (cleaned.isNotEmpty) controller.note('예전 구조의 프로그램 파일 정리 (${cleaned.length}개): ${cleaned.join(', ')}');
|
||||||
if (crashed != null) {
|
if (crashed != null) {
|
||||||
String hm(DateTime t) => '${t.month}/${t.day} ${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
String hm(DateTime t) => '${t.month}/${t.day} ${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
||||||
controller.note('⚠ 지난 실행이 정상적으로 끝나지 않았습니다 (시작 ${hm(crashed.$1)}, 마지막 확인 ${hm(crashed.$2)}). '
|
controller.note('⚠ 지난 실행이 정상적으로 끝나지 않았습니다 (시작 ${hm(crashed.$1)}, 마지막 확인 ${hm(crashed.$2)}). '
|
||||||
'그 전의 기록은 설정 폴더의 app.log 에 있습니다.');
|
'그 전의 기록은 Logs 폴더의 app.log 에 있습니다.');
|
||||||
}
|
}
|
||||||
await controller.init();
|
await controller.init();
|
||||||
|
|
||||||
|
|
@ -115,7 +122,6 @@ Future<void> main(List<String> args) async {
|
||||||
final ctx = navigatorKey.currentContext;
|
final ctx = navigatorKey.currentContext;
|
||||||
if (ctx != null && ctx.mounted && !await confirmExit(ctx, downloads)) return false;
|
if (ctx != null && ctx.mounted && !await confirmExit(ctx, downloads)) return false;
|
||||||
}
|
}
|
||||||
ExitTrace.file = p.join(dataDir, 'exit.log');
|
|
||||||
ExitTrace.start();
|
ExitTrace.start();
|
||||||
final job = controller.currentJob;
|
final job = controller.currentJob;
|
||||||
final wait = controller.pendingJobs.length;
|
final wait = controller.pendingJobs.length;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// 배포 폴더 구조
|
||||||
|
///
|
||||||
|
/// JJ_MKVMaker\jj_mkvmaker.exe 시작 프로그램 (Lib\jj_mkvmaker.exe 를 실행)
|
||||||
|
/// JJ_MKVMaker\LICENSE.txt · README.md
|
||||||
|
/// JJ_MKVMaker\Lib\ 실제 프로그램 · DLL · data · ffmpeg · tools (← 지금 실행 중인 exe 가 있는 곳)
|
||||||
|
/// JJ_MKVMaker\Logs\ 작업 기록 (app.log · exit.log)
|
||||||
|
///
|
||||||
|
/// 예전처럼 한 폴더에 모두 있는 구조 (exe 옆에 Lib 가 없음) 에서도 그대로 동작한다.
|
||||||
|
class AppPaths {
|
||||||
|
/// 지금 실행 중인 exe 가 있는 폴더 (프로그램 파일)
|
||||||
|
static String get exeDir => p.dirname(Platform.resolvedExecutable);
|
||||||
|
|
||||||
|
/// 배포 폴더 (맨 위). exe 가 Lib 안에 있으면 그 위 폴더.
|
||||||
|
static String get root {
|
||||||
|
final dir = exeDir;
|
||||||
|
return p.basename(dir).toLowerCase() == 'lib' ? p.dirname(dir) : dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lib 구조로 실행 중인지
|
||||||
|
static bool get inLib => root != exeDir;
|
||||||
|
|
||||||
|
/// 작업 기록 폴더: 배포 폴더의 Logs (쓸 수 없으면 [fallback] 아래 Logs)
|
||||||
|
static String logsDir(String fallback) {
|
||||||
|
for (final dir in [p.join(root, 'Logs'), p.join(fallback, 'Logs')]) {
|
||||||
|
try {
|
||||||
|
Directory(dir).createSync(recursive: true);
|
||||||
|
final probe = File(p.join(dir, '.jj_write_test'))..writeAsStringSync('x');
|
||||||
|
probe.deleteSync();
|
||||||
|
return dir;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 예전 구조 (한 폴더) 에서 Lib 구조로 업데이트한 뒤 배포 폴더 맨 위에 남은 예전 프로그램 파일을 지운다.
|
||||||
|
/// 지우는 것: 프로그램이 넣었던 것으로 확인되는 것만 (DLL · data · ffmpeg · tools · 고지 파일).
|
||||||
|
/// 받은 동영상 · 모델 · 사용자가 둔 파일은 건드리지 않는다. 지운 이름 목록을 돌려준다.
|
||||||
|
static List<String> cleanupOldLayout() {
|
||||||
|
if (!inLib) return const [];
|
||||||
|
final top = root;
|
||||||
|
final removed = <String>[];
|
||||||
|
void del(FileSystemEntity e) {
|
||||||
|
try {
|
||||||
|
e.deleteSync(recursive: true);
|
||||||
|
removed.add(p.basename(e.path));
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 맨 위의 DLL: Lib 에도 같은 이름이 있는 것만 (= 프로그램 부속품)
|
||||||
|
try {
|
||||||
|
for (final e in Directory(top).listSync()) {
|
||||||
|
final name = p.basename(e.path);
|
||||||
|
final lower = name.toLowerCase();
|
||||||
|
if (e is File &&
|
||||||
|
(lower.endsWith('.dll') || lower == 'third_party_notices.txt' || lower == 'native_assets.json') &&
|
||||||
|
File(p.join(exeDir, name)).existsSync()) {
|
||||||
|
del(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
// 폴더: 프로그램 것인지 안의 대표 파일로 확인
|
||||||
|
for (final (dir, marker) in [('data', 'app.so'), ('ffmpeg', 'ffmpeg.exe'), ('tools', 'yt-dlp.exe')]) {
|
||||||
|
final d = Directory(p.join(top, dir));
|
||||||
|
if (d.existsSync() && File(p.join(d.path, marker)).existsSync() && Directory(p.join(exeDir, dir)).existsSync()) {
|
||||||
|
del(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,8 @@ import 'package:crypto/crypto.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'app_paths.dart';
|
||||||
|
|
||||||
import '../../core/app_update.dart';
|
import '../../core/app_update.dart';
|
||||||
import '../../services/updater.dart';
|
import '../../services/updater.dart';
|
||||||
|
|
||||||
|
|
@ -24,7 +26,7 @@ class GitHubUpdater implements Updater {
|
||||||
this.apiBase = 'https://api.github.com',
|
this.apiBase = 'https://api.github.com',
|
||||||
String? appDir,
|
String? appDir,
|
||||||
this.versionOverride,
|
this.versionOverride,
|
||||||
}) : appDir = appDir ?? p.dirname(Platform.resolvedExecutable);
|
}) : appDir = appDir ?? AppPaths.root; // Lib 구조면 배포 폴더 맨 위
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> currentVersion() async {
|
Future<String> currentVersion() async {
|
||||||
|
|
@ -98,13 +100,16 @@ class GitHubUpdater implements Updater {
|
||||||
if (t.exitCode != 0) throw UpdateException('압축을 풀 수 없습니다: ${t.stderr}');
|
if (t.exitCode != 0) throw UpdateException('압축을 풀 수 없습니다: ${t.stderr}');
|
||||||
await zip.delete();
|
await zip.delete();
|
||||||
final exeName = p.basename(Platform.resolvedExecutable).toLowerCase();
|
final exeName = p.basename(Platform.resolvedExecutable).toLowerCase();
|
||||||
final exe = await out
|
// 배포 폴더 맨 위 = 가장 얕은 곳의 jj_mkvmaker.exe (Lib 구조면 시작 프로그램, 예전 구조면 프로그램 자신)
|
||||||
|
final exes = await out
|
||||||
.list(recursive: true)
|
.list(recursive: true)
|
||||||
.firstWhere((e) => e is File && p.basename(e.path).toLowerCase() == 'jj_mkvmaker.exe' ||
|
.where((e) => e is File && {'jj_mkvmaker.exe', exeName}.contains(p.basename(e.path).toLowerCase()))
|
||||||
e is File && p.basename(e.path).toLowerCase() == exeName,
|
.map((e) => e.path)
|
||||||
orElse: () => throw const UpdateException('압축 파일 안에 프로그램이 없습니다.'));
|
.toList();
|
||||||
|
if (exes.isEmpty) throw const UpdateException('압축 파일 안에 프로그램이 없습니다.');
|
||||||
|
exes.sort((a, b) => p.split(a).length.compareTo(p.split(b).length));
|
||||||
onProgress(1);
|
onProgress(1);
|
||||||
return p.dirname(exe.path);
|
return p.dirname(exes.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 파일 교체 스크립트 (영문만: Windows PowerShell 5.1 이 BOM 없는 UTF-8 을 잘못 읽는 문제 방지)
|
/// 파일 교체 스크립트 (영문만: Windows PowerShell 5.1 이 BOM 없는 UTF-8 을 잘못 읽는 문제 방지)
|
||||||
|
|
@ -130,7 +135,7 @@ exit 0
|
||||||
[
|
[
|
||||||
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', ps1.path,
|
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', ps1.path,
|
||||||
'-ParentPid', '$waitPid', '-Source', source, '-Target', appDir,
|
'-ParentPid', '$waitPid', '-Source', source, '-Target', appDir,
|
||||||
'-Exe', p.join(appDir, p.basename(Platform.resolvedExecutable)),
|
'-Exe', p.join(appDir, 'jj_mkvmaker.exe'),
|
||||||
if (!restart) '-NoRestart',
|
if (!restart) '-NoRestart',
|
||||||
],
|
],
|
||||||
mode: restart ? ProcessStartMode.detached : ProcessStartMode.normal,
|
mode: restart ? ProcessStartMode.detached : ProcessStartMode.normal,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2026.10.1+1
|
version: 2026.10.1+2
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.13.4
|
sdk: ^3.13.4
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,8 @@ void main() {
|
||||||
// 놓기
|
// 놓기
|
||||||
await tester.runAsync(() async {
|
await tester.runAsync(() async {
|
||||||
await _native(tester, 'performOperation', [file]);
|
await _native(tester, 'performOperation', [file]);
|
||||||
for (var i = 0; i < 50 && c.videos.isEmpty; i++) {
|
// PC 가 바쁠 때 (전체 테스트 중) 를 생각해 넉넉히 최대 5초
|
||||||
|
for (var i = 0; i < 250 && c.videos.isEmpty; i++) {
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||||
}
|
}
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
@ -93,7 +94,7 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.runAsync(() async {
|
await tester.runAsync(() async {
|
||||||
await _native(tester, 'performOperation', [file]);
|
await _native(tester, 'performOperation', [file]);
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
await Future<void>.delayed(const Duration(milliseconds: 1000));
|
||||||
});
|
});
|
||||||
// 앞의 안내가 사라지고 새 안내가 나올 때까지
|
// 앞의 안내가 사라지고 새 안내가 나올 때까지
|
||||||
for (var i = 0; i < 10; i++) {
|
for (var i = 0; i < 10; i++) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# 배포 폴더 맨 위의 시작 프로그램 (jj_mkvmaker.exe → Lib\jj_mkvmaker.exe) 을 빌드한다.
|
||||||
|
# 결과: build\launcher\jj_mkvmaker.exe (C 런타임 정적 연결 - 다른 DLL 없이 실행)
|
||||||
|
#
|
||||||
|
# 사용: powershell -File tool\build_launcher.ps1 [-VsDir <Visual Studio Build Tools 폴더>]
|
||||||
|
param([string]$VsDir = '')
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$app = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
if (-not $VsDir) { $VsDir = Join-Path $app '..\tools\VSBuildTools' } # PS 5.1: $PSScriptRoot is empty in param defaults
|
||||||
|
$src = Join-Path $app 'windows\launcher'
|
||||||
|
$out = Join-Path $app 'build\launcher'
|
||||||
|
New-Item -ItemType Directory -Force $out | Out-Null
|
||||||
|
|
||||||
|
$vcvars = Get-ChildItem $VsDir -Recurse -Filter vcvars64.bat -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if (-not $vcvars) { throw "vcvars64.bat not found: $VsDir" }
|
||||||
|
|
||||||
|
$cmd = @"
|
||||||
|
call "$($vcvars.FullName)" >nul
|
||||||
|
cd /d "$out"
|
||||||
|
rc /nologo /fo launcher.res "$src\launcher.rc" || exit /b 1
|
||||||
|
cl /nologo /O2 /MT /EHsc /std:c++17 /utf-8 /DUNICODE /D_UNICODE "$src\launcher.cpp" launcher.res /Fe:jj_mkvmaker.exe /link /SUBSYSTEM:WINDOWS user32.lib shell32.lib || exit /b 1
|
||||||
|
"@
|
||||||
|
$bat = Join-Path $out 'build.cmd'
|
||||||
|
[IO.File]::WriteAllText($bat, $cmd, [Text.Encoding]::Default)
|
||||||
|
cmd /c $bat
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "launcher build failed" }
|
||||||
|
Join-Path $out 'jj_mkvmaker.exe'
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Build the distribution layout from a Flutter release build:
|
||||||
|
#
|
||||||
|
# <Target>\jj_mkvmaker.exe launcher (tool\build_launcher.ps1) -> runs Lib\jj_mkvmaker.exe
|
||||||
|
# <Target>\LICENSE.txt, README.md
|
||||||
|
# <Target>\Lib\... everything from build\windows\x64\runner\Release
|
||||||
|
# <Target>\Logs\ created by the app at run time
|
||||||
|
#
|
||||||
|
# -Clean : make <Target> exactly this layout (for the release zip stage folder)
|
||||||
|
# default: update in place (deploy folder) - keeps downloads, models, Logs and other user files,
|
||||||
|
# and removes program files left at the top by the old single-folder layout.
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Target,
|
||||||
|
[string]$Release = '',
|
||||||
|
[switch]$Clean
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$app = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
if (-not $Release) { $Release = Join-Path $app 'build\windows\x64\runner\Release' } # PS 5.1: $PSScriptRoot is empty in param defaults
|
||||||
|
$Release = (Resolve-Path $Release).Path
|
||||||
|
$launcher = Join-Path $app 'build\launcher\jj_mkvmaker.exe'
|
||||||
|
if (-not (Test-Path $launcher)) { $launcher = & (Join-Path $PSScriptRoot 'build_launcher.ps1') | Select-Object -Last 1 }
|
||||||
|
New-Item -ItemType Directory -Force $Target | Out-Null
|
||||||
|
$lib = Join-Path $Target 'Lib'
|
||||||
|
|
||||||
|
if ($Clean) {
|
||||||
|
robocopy $Release $lib /E /PURGE /XD jj_yt-dlp jj_aria2 models /NFL /NDL /NJH /NJS /NP | Out-Null
|
||||||
|
# nothing else at the top
|
||||||
|
Get-ChildItem $Target | Where-Object { $_.Name -notin 'Lib' } | Remove-Item -Recurse -Force
|
||||||
|
} else {
|
||||||
|
robocopy $Release $lib /E /XD jj_yt-dlp jj_aria2 models /NFL /NDL /NJH /NJS /NP | Out-Null
|
||||||
|
# old single-folder layout: program files at the top that now live in Lib
|
||||||
|
Get-ChildItem $Target -File | Where-Object {
|
||||||
|
($_.Extension -eq '.dll' -or $_.Name -in 'THIRD_PARTY_NOTICES.txt', 'native_assets.json') -and (Test-Path (Join-Path $lib $_.Name))
|
||||||
|
} | Remove-Item -Force
|
||||||
|
foreach ($pair in @(@('data', 'app.so'), @('ffmpeg', 'ffmpeg.exe'), @('tools', 'yt-dlp.exe'))) {
|
||||||
|
$d = Join-Path $Target $pair[0]
|
||||||
|
if ((Test-Path (Join-Path $d $pair[1])) -and (Test-Path (Join-Path $lib $pair[0]))) { Remove-Item $d -Recurse -Force }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# debug build leftover (integration tests) must never ship
|
||||||
|
Remove-Item (Join-Path $lib 'data\flutter_assets\kernel_blob.bin') -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Copy-Item $launcher (Join-Path $Target 'jj_mkvmaker.exe') -Force
|
||||||
|
Copy-Item (Join-Path $lib 'LICENSE.txt') (Join-Path $Target 'LICENSE.txt') -Force
|
||||||
|
Copy-Item (Join-Path $app 'README.md') (Join-Path $Target 'README.md') -Force
|
||||||
|
Get-ChildItem $Target | Select-Object -ExpandProperty Name
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
// JJ_MKVMaker 시작 프로그램 (배포 폴더 맨 위의 jj_mkvmaker.exe)
|
||||||
|
//
|
||||||
|
// 배포 폴더를 깔끔하게 두려고 실제 프로그램과 부속 파일은 모두 Lib\ 아래에 있다.
|
||||||
|
// JJ_MKVMaker\jj_mkvmaker.exe ← 이 파일 (Lib\jj_mkvmaker.exe 를 같은 인수로 실행하고 끝남)
|
||||||
|
// JJ_MKVMaker\LICENSE.txt, README.md
|
||||||
|
// JJ_MKVMaker\Lib\... ← 프로그램 · DLL · ffmpeg · 도구
|
||||||
|
// JJ_MKVMaker\Logs\... ← 작업 기록
|
||||||
|
//
|
||||||
|
// 빌드: tool\build_launcher.ps1 (Visual Studio Build Tools 의 cl · rc, C 런타임 정적 연결 → 다른 DLL 없이 실행)
|
||||||
|
#include <windows.h>
|
||||||
|
#include <shellapi.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
static std::wstring ExeDir() {
|
||||||
|
wchar_t buf[MAX_PATH * 4];
|
||||||
|
const DWORD n = GetModuleFileNameW(nullptr, buf, static_cast<DWORD>(sizeof(buf) / sizeof(buf[0])));
|
||||||
|
std::wstring path(buf, n);
|
||||||
|
const size_t slash = path.find_last_of(L"\\/");
|
||||||
|
return slash == std::wstring::npos ? L"." : path.substr(0, slash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이 프로그램을 실행할 때 받은 인수 (프로그램 이름 뒤 전부, 따옴표 포함 그대로)
|
||||||
|
static std::wstring ArgsAfterProgramName() {
|
||||||
|
const wchar_t* cmd = GetCommandLineW();
|
||||||
|
bool quoted = false;
|
||||||
|
while (*cmd && (quoted || (*cmd != L' ' && *cmd != L'\t'))) {
|
||||||
|
if (*cmd == L'"') quoted = !quoted;
|
||||||
|
++cmd;
|
||||||
|
}
|
||||||
|
while (*cmd == L' ' || *cmd == L'\t') ++cmd;
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int show) {
|
||||||
|
const std::wstring lib = ExeDir() + L"\\Lib";
|
||||||
|
const std::wstring app = lib + L"\\jj_mkvmaker.exe";
|
||||||
|
if (GetFileAttributesW(app.c_str()) == INVALID_FILE_ATTRIBUTES) {
|
||||||
|
const std::wstring msg = L"프로그램 파일을 찾을 수 없습니다:\n" + app +
|
||||||
|
L"\n\n배포 zip 을 다시 풀어 Lib 폴더가 jj_mkvmaker.exe 옆에 있는지 확인하세요.";
|
||||||
|
MessageBoxW(nullptr, msg.c_str(), L"JJ_MKVMaker", MB_ICONERROR | MB_OK);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring cmdline = L"\"" + app + L"\"";
|
||||||
|
const std::wstring args = ArgsAfterProgramName();
|
||||||
|
if (!args.empty()) cmdline += L" " + args;
|
||||||
|
|
||||||
|
// 새로 뜨는 창이 앞으로 올 수 있게
|
||||||
|
AllowSetForegroundWindow(ASFW_ANY);
|
||||||
|
|
||||||
|
STARTUPINFOW si{};
|
||||||
|
si.cb = sizeof(si);
|
||||||
|
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||||
|
si.wShowWindow = static_cast<WORD>(show);
|
||||||
|
PROCESS_INFORMATION pi{};
|
||||||
|
if (!CreateProcessW(app.c_str(), cmdline.data(), nullptr, nullptr, FALSE, 0, nullptr, lib.c_str(), &si, &pi)) {
|
||||||
|
const std::wstring msg = L"프로그램을 시작할 수 없습니다 (오류 " + std::to_wstring(GetLastError()) + L"):\n" + app;
|
||||||
|
MessageBoxW(nullptr, msg.c_str(), L"JJ_MKVMaker", MB_ICONERROR | MB_OK);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
CloseHandle(pi.hProcess);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// 시작 프로그램의 아이콘 · 버전 정보
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
1 ICON "..\\runner\\resources\\app_icon.ico"
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION 1,0,0,0
|
||||||
|
PRODUCTVERSION 1,0,0,0
|
||||||
|
FILEFLAGSMASK 0x3fL
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
FILEOS VOS__WINDOWS32
|
||||||
|
FILETYPE VFT_APP
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "041204b0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "RyuJongJin"
|
||||||
|
VALUE "FileDescription", "JJ_MKVMaker"
|
||||||
|
VALUE "InternalName", "jj_mkvmaker"
|
||||||
|
VALUE "OriginalFilename", "jj_mkvmaker.exe"
|
||||||
|
VALUE "ProductName", "JJ_MKVMaker"
|
||||||
|
VALUE "LegalCopyright", "MIT License"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x412, 1200
|
||||||
|
END
|
||||||
|
END
|
||||||
Loading…
Reference in New Issue