Skip to content

Commit ed93cc4

Browse files
committed
v0.2.0: exe 자동 업데이트 + self-dependency 버그 수정
selfUpdate 모듈 추가 — GitHub Releases에서 최신 exe 감지하여 자동 다운로드 및 교체 (old → new rename 패턴). uv init → uv venv + uv pip install 방식으로 전환. python -m dartlab → Scripts/dartlab.exe 직접 실행.
1 parent 76b86fc commit ed93cc4

5 files changed

Lines changed: 135 additions & 5 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dartlab-desktop"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2024"
55
description = "DartLab AI Desktop Launcher for Windows"
66
license = "MIT"

src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ mod updater;
44
mod ollama;
55
mod paths;
66
mod ui;
7+
#[allow(non_snake_case)]
8+
mod selfUpdate;
79

810
use std::process::ExitCode;
911

1012
fn main() -> ExitCode {
13+
selfUpdate::cleanup_old();
14+
1115
ui::print_banner();
1216

17+
selfUpdate::check_and_update();
18+
1319
let app_dir = paths::app_dir();
1420
if !app_dir.exists() {
1521
std::fs::create_dir_all(&app_dir).expect("Failed to create app directory");

src/paths.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ pub fn venv_dir(app_dir: &std::path::Path) -> PathBuf {
1717
pub fn python_bin(app_dir: &std::path::Path) -> PathBuf {
1818
venv_dir(app_dir).join("Scripts").join("python.exe")
1919
}
20+
21+
pub fn dartlab_bin(app_dir: &std::path::Path) -> PathBuf {
22+
venv_dir(app_dir).join("Scripts").join("dartlab.exe")
23+
}

src/runner.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const PORT: u16 = 8400;
66
const URL: &str = "http://localhost:8400";
77

88
pub fn run_dartlab(app_dir: &Path) -> Result<(), String> {
9-
let python = paths::python_bin(app_dir);
9+
let dartlab = paths::dartlab_bin(app_dir);
1010

1111
ui::print_info(&format!("브라우저에서 {URL} 을 엽니다"));
1212
ui::print_info("종료하려면 이 창을 닫으세요");
@@ -17,10 +17,8 @@ pub fn run_dartlab(app_dir: &Path) -> Result<(), String> {
1717
open::that(URL).ok();
1818
});
1919

20-
let status = Command::new(&python)
20+
let status = Command::new(&dartlab)
2121
.args([
22-
"-m",
23-
"dartlab",
2422
"ai",
2523
"--port",
2624
&PORT.to_string(),

src/selfUpdate.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use std::path::PathBuf;
2+
use crate::ui;
3+
4+
const GITHUB_API: &str = "https://api.github.com/repos/eddmpython/dartlab-desktop/releases/latest";
5+
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
6+
7+
pub fn cleanup_old() {
8+
let current = std::env::current_exe().ok();
9+
if let Some(ref exe) = current {
10+
let old = exe.with_extension("exe.old");
11+
if old.exists() {
12+
std::fs::remove_file(&old).ok();
13+
}
14+
}
15+
}
16+
17+
pub fn check_and_update() {
18+
let latest = match get_latest_release() {
19+
Ok(r) => r,
20+
Err(_) => return,
21+
};
22+
23+
let current = format!("v{CURRENT_VERSION}");
24+
if latest.tag == current {
25+
return;
26+
}
27+
28+
ui::print_info(&format!("런처 업데이트 발견: {current} → {}", latest.tag));
29+
30+
let download_url = match latest.asset_url {
31+
Some(url) => url,
32+
None => return,
33+
};
34+
35+
let current_exe = match std::env::current_exe() {
36+
Ok(p) => p,
37+
Err(_) => return,
38+
};
39+
40+
let new_exe = current_exe.with_extension("exe.new");
41+
let old_exe = current_exe.with_extension("exe.old");
42+
43+
if let Err(e) = download_file(&download_url, &new_exe) {
44+
ui::print_warn(&format!("런처 다운로드 실패: {e}"));
45+
std::fs::remove_file(&new_exe).ok();
46+
return;
47+
}
48+
49+
if std::fs::rename(&current_exe, &old_exe).is_err() {
50+
ui::print_warn("런처 교체 실패 (권한 부족)");
51+
std::fs::remove_file(&new_exe).ok();
52+
return;
53+
}
54+
55+
if std::fs::rename(&new_exe, &current_exe).is_err() {
56+
std::fs::rename(&old_exe, &current_exe).ok();
57+
ui::print_warn("런처 교체 실패");
58+
return;
59+
}
60+
61+
ui::print_ok(&format!("런처 업데이트 완료 ({})", latest.tag));
62+
ui::print_info("다음 실행 시 새 버전이 적용됩니다");
63+
}
64+
65+
struct ReleaseInfo {
66+
tag: String,
67+
asset_url: Option<String>,
68+
}
69+
70+
fn get_latest_release() -> Result<ReleaseInfo, String> {
71+
let client = reqwest::blocking::Client::builder()
72+
.user_agent("dartlab-desktop")
73+
.timeout(std::time::Duration::from_secs(5))
74+
.build()
75+
.map_err(|e| e.to_string())?;
76+
77+
let resp = client.get(GITHUB_API).send().map_err(|e| e.to_string())?;
78+
79+
if !resp.status().is_success() {
80+
return Err(format!("HTTP {}", resp.status()));
81+
}
82+
83+
let body = resp.text().map_err(|e| e.to_string())?;
84+
let json: serde_json::Value = serde_json::from_str(&body).map_err(|e| e.to_string())?;
85+
86+
let tag = json["tag_name"]
87+
.as_str()
88+
.ok_or("No tag_name")?
89+
.to_string();
90+
91+
let asset_url = json["assets"]
92+
.as_array()
93+
.and_then(|assets| {
94+
assets.iter().find_map(|a| {
95+
let name = a["name"].as_str()?;
96+
if name == "DartLab.exe" {
97+
a["browser_download_url"].as_str().map(|s| s.to_string())
98+
} else {
99+
None
100+
}
101+
})
102+
});
103+
104+
Ok(ReleaseInfo { tag, asset_url })
105+
}
106+
107+
fn download_file(url: &str, dest: &PathBuf) -> Result<(), String> {
108+
let client = reqwest::blocking::Client::builder()
109+
.user_agent("dartlab-desktop")
110+
.build()
111+
.map_err(|e| e.to_string())?;
112+
113+
let resp = client.get(url).send().map_err(|e| format!("Download failed: {e}"))?;
114+
115+
if !resp.status().is_success() {
116+
return Err(format!("HTTP {}", resp.status()));
117+
}
118+
119+
let bytes = resp.bytes().map_err(|e| e.to_string())?;
120+
std::fs::write(dest, &bytes).map_err(|e| e.to_string())?;
121+
Ok(())
122+
}

0 commit comments

Comments
 (0)