1 parent
3071e0d1ef
commit
916b79e5d6
12 files changed
+230
-40
No files matched your search
Vendored
+1
-1
@@ -67,7 +67,7 @@
|
||||
"id": "projectPath",
|
||||
"type": "promptString",
|
||||
"description": "프로젝트 JSON 경로 (절대 경로 또는 src 기준 상대 경로, 따옴표 없이 입력)",
|
||||
"default": "fixtures/package-project.json"
|
||||
"default": "fixtures/InstallerProject.isproj"
|
||||
},
|
||||
{
|
||||
"id": "targetId",
|
||||
|
||||
+10
-10
@@ -12,7 +12,7 @@ npm.cmd ci
|
||||
npm.cmd run tauri dev
|
||||
```
|
||||
|
||||
새 프로젝트를 만들거나 `.json` 파일을 연 뒤 앱 정보 → 파일 구성에서 소스 폴더 연결 → 패키지 설정 → 검증·빌드에서 SDK 도구 선택 → 입력 검사 → 빌드 계획 생성 → MSIX 빌드 순서로 사용한다. 포함/제외 규칙과 의존성은 프로젝트 JSON 편집에서 수정한다. 기본 프로젝트의 Publisher는 예시이며 자신의 패키지 identity에 맞게 바꿔야 한다.
|
||||
새 프로젝트를 만들거나 `.isproj` 파일을 연 뒤 앱 정보 → 파일 구성에서 소스 폴더 연결 → 패키지 설정 → 검증·빌드에서 SDK 도구 선택 → 입력 검사 → 빌드 계획 생성 → MSIX 빌드 순서로 사용한다. 포함/제외 규칙과 의존성은 프로젝트 JSON 편집에서 수정한다. 기본 프로젝트의 Publisher는 예시이며 자신의 패키지 identity에 맞게 바꿔야 한다.
|
||||
|
||||
시작 화면은 왼쪽 **최근 프로젝트 목록**, 오른쪽 **프로젝트 열기 / 새 프로젝트 만들기** 버튼으로 구성한다. 최근 목록은 이 버전의 GUI에서 열거나 만든 프로젝트를 최대 20개 보관하며 앱을 다시 실행해도 유지된다. 이름·경로 검색과 오늘/이번 주/이번 달/이전 그룹을 제공한다. 목록의 `×`는 기록만 제거하며 프로젝트 파일을 삭제하지 않는다. 이동·삭제된 파일은 위치 확인 안내를 표시한다.
|
||||
|
||||
@@ -52,7 +52,7 @@ VS Code 빌드/시험 산출물은 `src/target/vscode`에 분리한다. 기존 `
|
||||
| `MSIX CLI: Plan release` | 프로젝트 경로와 대상 ID를 받아 계획 생성 |
|
||||
| `MSIX CLI: Build plan` | 프로젝트 경로와 새 계획 UUID를 받아 실제 MSIX 생성 |
|
||||
|
||||
CLI 입력 경로는 절대 경로나 `src` 기준 상대 경로를 따옴표 없이 입력한다. 기본값 `fixtures/package-project.json`은 Publisher가 미확정인 초안이므로 검증 오류를 확인하는 용도다. 실제 빌드는 소스/SDK가 연결된 프로젝트를 사용한다. 시험 앱을 생성했다면 `.work/demo/FixtureAlpha/package-project.json`을 입력할 수 있다. 계획 ID는 `Plan release` 결과의 `id`를 사용한다.
|
||||
CLI 입력 경로는 절대 경로나 `src` 기준 상대 경로를 따옴표 없이 입력한다. 기본값 `fixtures/InstallerProject.isproj`은 Publisher가 미확정인 초안이므로 검증 오류를 확인하는 용도다. 실제 빌드는 소스/SDK가 연결된 프로젝트를 사용한다. 시험 앱을 생성했다면 `.work/demo/FixtureAlpha/InstallerProject.isproj`을 입력할 수 있다. 계획 ID는 `Plan release` 결과의 `id`를 사용한다.
|
||||
|
||||
명령 팔레트의 `Tasks: Run Task`에서 `MSIX: test Rust`를 실행할 수 있다. MakeAppx 통합 시험은 기존 `scripts/Check.ps1 -MakeAppx ...`로 명시 실행한다.
|
||||
|
||||
@@ -69,20 +69,20 @@ HKCU 초기화·COM/파일 연결·ISM 가져오기와 MSI 속성 치환은 아
|
||||
아래 명령은 `src`에서 실행한다. stdout은 JSON, 오류는 stderr JSON이다. 개발자 서명 모드는 실제 MSIX 서명을 수행한다. 앱 설치와 PC의 인증서 신뢰 등록은 자동 수행하지 않는다.
|
||||
|
||||
```powershell
|
||||
cargo run -p package-builder -- init --project D:\packages\demo\package-project.json
|
||||
cargo run -p package-builder -- validate --project D:\packages\demo\package-project.json
|
||||
cargo run -p package-builder -- plan release --project D:\packages\demo\package-project.json --target msix-direct
|
||||
cargo run -p package-builder -- build --project D:\packages\demo\package-project.json --plan-id <계획의-id>
|
||||
cargo run -p package-builder -- job status --project D:\packages\demo\package-project.json --id <계획의-id>
|
||||
cargo run -p package-builder -- export --project D:\packages\demo\package-project.json --release <계획의-id> --output D:\exports\new-folder
|
||||
cargo run -p package-builder -- init --project D:\packages\demo\InstallerProject.isproj
|
||||
cargo run -p package-builder -- validate --project D:\packages\demo\InstallerProject.isproj
|
||||
cargo run -p package-builder -- plan release --project D:\packages\demo\InstallerProject.isproj --target msix-direct
|
||||
cargo run -p package-builder -- build --project D:\packages\demo\InstallerProject.isproj --plan-id <계획의-id>
|
||||
cargo run -p package-builder -- job status --project D:\packages\demo\InstallerProject.isproj --id <계획의-id>
|
||||
cargo run -p package-builder -- export --project D:\packages\demo\InstallerProject.isproj --release <계획의-id> --output D:\exports\new-folder
|
||||
cargo run -p package-builder -- schema
|
||||
```
|
||||
|
||||
프로젝트는 `.json`만 지원하며 내부 내용은 UTF-8 JSON v2다. 새 프로젝트의 기본 이름은 `package-project.json`이고, 확장자를 생략하면 `.json`를 붙인다. 기존 `.json` 프로젝트의 열기·호환·변환은 제공하지 않는다. 기존 JSON 항목은 최근 목록에서도 숨긴다.
|
||||
프로젝트는 `.isproj`만 지원하며 내부 내용은 UTF-8 JSON v2다. 새 프로젝트의 기본 이름은 `InstallerProject.isproj`이고, 확장자를 생략하면 `.isproj`를 붙인다. 기존 `.json` 프로젝트의 열기·호환·변환은 제공하지 않는다. 기존 JSON 항목은 최근 목록에서도 숨긴다.
|
||||
|
||||
별도의 프로젝트 ID는 사용하지 않는다. MSIX 앱 계열은 패키지 설정의 Identity Name과 Publisher로 관리한다. 기존 `projectId` 필드가 포함된 개발용 파일은 자동 변환하지 않고 알 수 없는 필드로 거부한다. 현재 fixture와 샘플 생성 스크립트는 해당 필드 없이 프로젝트를 만든다.
|
||||
|
||||
프로젝트 옆 `<프로젝트 파일명>.local.json`에서 소스 alias와 SDK를 연결한다. 예를 들어 `package-project.local.json`이다. 상대 소스 경로는 프로젝트 폴더 기준이다. 기존 `package-project.local.json`은 읽지 않으며 local 파일은 Git에 넣지 않는다. 저장 시 이전 프로젝트는 `.json.bak`, 이전 로컬 설정은 `.json.local.json.bak`으로 보관한다.
|
||||
프로젝트 옆 `<프로젝트 파일명>.local.json`에서 소스 alias와 SDK를 연결한다. 예를 들어 `InstallerProject.isproj.local.json`이다. 상대 소스 경로는 프로젝트 폴더 기준이다. 기존 `package-project.local.json`은 읽지 않으며 local 파일은 Git에 넣지 않는다. 저장 시 이전 프로젝트는 `.isproj.bak`, 이전 로컬 설정은 `.isproj.local.json.bak`으로 보관한다.
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -121,7 +121,7 @@ async fn open_project(
|
||||
let Some(file) = rfd::AsyncFileDialog::new()
|
||||
.set_parent(&window)
|
||||
.set_title("프로젝트 열기")
|
||||
.add_filter("Installer Studio 프로젝트", &["json"])
|
||||
.add_filter("Installer Studio 프로젝트", &["isproj"])
|
||||
.pick_file()
|
||||
.await
|
||||
else {
|
||||
@@ -142,8 +142,8 @@ async fn new_project(
|
||||
let Some(file) = rfd::AsyncFileDialog::new()
|
||||
.set_parent(&window)
|
||||
.set_title("새 프로젝트 만들기")
|
||||
.set_file_name("package-project.json")
|
||||
.add_filter("Installer Studio 프로젝트", &["json"])
|
||||
.set_file_name("InstallerProject.isproj")
|
||||
.add_filter("Installer Studio 프로젝트", &["isproj"])
|
||||
.save_file()
|
||||
.await
|
||||
else {
|
||||
@@ -337,7 +337,7 @@ mod tests {
|
||||
#[test]
|
||||
fn close_clears_the_session_without_changing_the_project() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let path = temp.path().join("project.json");
|
||||
let path = temp.path().join("project.isproj");
|
||||
let doc = infra::create_project(&path).unwrap();
|
||||
let original = std::fs::read(&path).unwrap();
|
||||
let session = Session::default();
|
||||
@@ -350,7 +350,7 @@ mod tests {
|
||||
#[test]
|
||||
fn close_is_rejected_while_build_is_running() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let doc = infra::create_project(&temp.path().join("project.json")).unwrap();
|
||||
let doc = infra::create_project(&temp.path().join("project.isproj")).unwrap();
|
||||
let session = Session::default();
|
||||
set_document(&session, doc).unwrap();
|
||||
session.busy.store(true, Ordering::SeqCst);
|
||||
|
||||
@@ -74,6 +74,7 @@ impl RecentStore {
|
||||
Ok(history
|
||||
.projects
|
||||
.iter()
|
||||
.filter(|project| package_infra::is_project_path(&project.path))
|
||||
.map(|project| RecentView {
|
||||
available: project.path.is_file(),
|
||||
project: project.clone(),
|
||||
@@ -125,7 +126,7 @@ impl RecentStore {
|
||||
history
|
||||
.projects
|
||||
.iter()
|
||||
.find(|entry| entry.id == id)
|
||||
.find(|entry| entry.id == id && package_infra::is_project_path(&entry.path))
|
||||
.map(|entry| entry.path.clone())
|
||||
.context("최근 목록에서 프로젝트를 찾을 수 없습니다. 목록을 새로 고치세요.")
|
||||
},
|
||||
@@ -163,8 +164,8 @@ mod tests {
|
||||
fn history_persists_deduplicates_and_removal_keeps_project_file() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let store = RecentStore(temp.path().join("history"));
|
||||
let mut first = package_infra::create_project(&temp.path().join("first.json")).unwrap();
|
||||
let second = package_infra::create_project(&temp.path().join("second.json")).unwrap();
|
||||
let mut first = package_infra::create_project(&temp.path().join("first.isproj")).unwrap();
|
||||
let second = package_infra::create_project(&temp.path().join("second.isproj")).unwrap();
|
||||
store.remember(&first, true).unwrap();
|
||||
let first_id = store.list().unwrap()[0].project.id.clone();
|
||||
store.remember(&second, true).unwrap();
|
||||
@@ -186,14 +187,14 @@ mod tests {
|
||||
fn history_is_bounded_and_missing_files_are_visible() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let store = RecentStore(temp.path().join("history"));
|
||||
let mut doc = package_infra::create_project(&temp.path().join("fixture.json")).unwrap();
|
||||
let mut doc = package_infra::create_project(&temp.path().join("fixture.isproj")).unwrap();
|
||||
for i in 0..25 {
|
||||
doc.path = temp.path().join(format!("missing-{i}.json"));
|
||||
doc.path = temp.path().join(format!("missing-{i}.isproj"));
|
||||
store.remember(&doc, true).unwrap();
|
||||
}
|
||||
let entries = store.list().unwrap();
|
||||
assert_eq!(entries.len(), MAX_RECENT);
|
||||
assert!(entries[0].project.path.ends_with("missing-24.json"));
|
||||
assert!(entries[0].project.path.ends_with("missing-24.isproj"));
|
||||
assert!(entries.iter().all(|entry| !entry.available));
|
||||
}
|
||||
|
||||
@@ -204,10 +205,22 @@ mod tests {
|
||||
fs::create_dir_all(&store.0).unwrap();
|
||||
let history_path = store.0.join("recent-projects.json");
|
||||
fs::write(&history_path, b"broken").unwrap();
|
||||
let doc = package_infra::create_project(&temp.path().join("fixture.json")).unwrap();
|
||||
let doc = package_infra::create_project(&temp.path().join("fixture.isproj")).unwrap();
|
||||
assert!(store.remember(&doc, true).is_err());
|
||||
assert_eq!(fs::read(history_path).unwrap(), b"broken");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn legacy_history_is_hidden_and_cannot_be_opened_by_id() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let store = RecentStore(temp.path().join("history"));
|
||||
let mut doc = package_infra::create_project(&temp.path().join("current.isproj")).unwrap();
|
||||
store.remember(&doc, true).unwrap();
|
||||
doc.path = temp.path().join("legacy.json");
|
||||
store.remember(&doc, true).unwrap();
|
||||
let id = store.load().unwrap().projects[0].id.clone();
|
||||
assert_eq!(store.list().unwrap().len(), 1);
|
||||
assert!(store.find(&id).is_err());
|
||||
assert_eq!(store.load().unwrap().projects.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { displayPath, groupRecentProjects, renderStartScreen } from '../src/star
|
||||
import { allowProjectTransition } from '../src/project-transition.ts';
|
||||
|
||||
const recent = (id, year, month, day, name = id) => ({
|
||||
id, path: `C:\\projects\\${id}\\package-project.json`, displayName: name,
|
||||
id, path: `C:\\projects\\${id}\\InstallerProject.isproj`, displayName: name,
|
||||
lastOpenedAt: new Date(year, month - 1, day, 10).getTime() / 1000, available: true,
|
||||
});
|
||||
|
||||
@@ -18,8 +18,8 @@ test('recent projects are grouped in local calendar order and searchable by name
|
||||
});
|
||||
|
||||
test('extended Windows paths are displayed without changing their UNC semantics', () => {
|
||||
assert.equal(displayPath('\\\\?\\C:\\apps\\project.json'), 'C:\\apps\\project.json');
|
||||
assert.equal(displayPath('\\\\?\\UNC\\server\\share\\project.json'), '\\\\server\\share\\project.json');
|
||||
assert.equal(displayPath('\\\\?\\C:\\apps\\project.isproj'), 'C:\\apps\\project.isproj');
|
||||
assert.equal(displayPath('\\\\?\\UNC\\server\\share\\project.isproj'), '\\\\server\\share\\project.isproj');
|
||||
});
|
||||
|
||||
test('start screen has left recent projects and right actions without fabricated entries', () => {
|
||||
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
#[test]
|
||||
fn example_requires_publisher_but_not_exe() {
|
||||
let project: Project =
|
||||
serde_json::from_str(include_str!("../../../fixtures/package-project.json"))
|
||||
serde_json::from_str(include_str!("../../../fixtures/InstallerProject.isproj"))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
validate(&project)
|
||||
|
||||
@@ -58,12 +58,39 @@ fn atomic_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
|
||||
temp.persist(path).map_err(|error| error.error)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn is_project_path(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("isproj"))
|
||||
&& !path.file_stem().is_some_and(|stem| {
|
||||
Path::new(stem)
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("isproj"))
|
||||
})
|
||||
}
|
||||
fn check_project_path(path: &Path) -> Result<()> {
|
||||
ensure!(is_project_path(path), "UnsupportedProjectExtension: .isproj 프로젝트만 지원합니다. 기존 .json 프로젝트는 지원하지 않습니다.");
|
||||
Ok(())
|
||||
}
|
||||
pub fn new_project_path(path: &Path) -> Result<PathBuf> {
|
||||
ensure!(path.file_name().is_some(), "MissingFilename");
|
||||
let path = if path.extension().is_none() {
|
||||
path.with_extension("isproj")
|
||||
} else {
|
||||
path.to_path_buf()
|
||||
};
|
||||
check_project_path(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
pub fn appended_path(path: &Path, suffix: &str) -> PathBuf {
|
||||
let mut name = path.as_os_str().to_os_string();
|
||||
name.push(suffix);
|
||||
PathBuf::from(name)
|
||||
}
|
||||
pub fn local_config_path(path: &Path) -> PathBuf {
|
||||
appended_path(path, ".local.json")
|
||||
}
|
||||
pub fn open_project(path: &Path) -> Result<ProjectDocument> {
|
||||
check_project_path(path)?;
|
||||
reject_links(path)?;
|
||||
let path = path.canonicalize()?;
|
||||
let bytes = fs::read(&path)?;
|
||||
@@ -73,7 +100,7 @@ pub fn open_project(path: &Path) -> Result<ProjectDocument> {
|
||||
project.schema_version == 2,
|
||||
"UnsupportedSchema: v2 프로젝트만 열 수 있습니다. 원본을 자동 변환하지 않습니다."
|
||||
);
|
||||
let local_path = path.parent().unwrap().join("package-project.local.json");
|
||||
let local_path = local_config_path(&path);
|
||||
reject_links(&local_path)?;
|
||||
let local_bytes = if local_path.exists() {
|
||||
fs::read(&local_path)?
|
||||
@@ -112,8 +139,8 @@ pub fn save_project(document: &ProjectDocument) -> Result<ProjectDocument> {
|
||||
);
|
||||
// A draft may be incomplete, but never silently accept a different schema.
|
||||
ensure!(document.project.schema_version == 2, "UnsupportedSchema");
|
||||
let project_backup = document.path.with_extension("json.bak");
|
||||
let local_path = document.path.parent().unwrap().join("package-project.local.json");
|
||||
let project_backup = appended_path(&document.path, ".bak");
|
||||
let local_path = local_config_path(&document.path);
|
||||
let local_backup = appended_path(&local_path, ".bak");
|
||||
for backup in [&project_backup, &local_backup] {
|
||||
reject_links(backup)?;
|
||||
@@ -141,12 +168,17 @@ pub fn save_project(document: &ProjectDocument) -> Result<ProjectDocument> {
|
||||
open_project(&document.path)
|
||||
}
|
||||
pub fn create_project(path: &Path) -> Result<ProjectDocument> {
|
||||
ensure!(path.file_name().is_some(), "MissingFilename");
|
||||
let normalized = new_project_path(path)?;
|
||||
let path = normalized.as_path();
|
||||
reject_links(path)?;
|
||||
fs::create_dir_all(path.parent().context("MissingParent")?)?;
|
||||
let _lock = lock_project(path.parent().unwrap())?;
|
||||
ensure!(
|
||||
!local_config_path(path).exists(),
|
||||
"LocalConfigAlreadyExists"
|
||||
);
|
||||
let mut value: serde_json::Value =
|
||||
serde_json::from_str(include_str!("../../../fixtures/package-project.json"))?;
|
||||
serde_json::from_str(include_str!("../../../fixtures/InstallerProject.isproj"))?;
|
||||
value["formats"]["msix"]["identity"]["publisher"] =
|
||||
serde_json::Value::String("CN=Example Publisher".into());
|
||||
let mut file = File::options()
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
use package_infra as infra;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn new_projects_normalize_extension_and_roundtrip_local_settings_and_backups() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let mut doc = infra::create_project(&temp.path().join("한글 프로젝트")).unwrap();
|
||||
assert!(doc.path.ends_with("한글 프로젝트.isproj"));
|
||||
let original = fs::read(&doc.path).unwrap();
|
||||
assert!(serde_json::from_slice::<serde_json::Value>(&original)
|
||||
.unwrap()
|
||||
.get("projectId")
|
||||
.is_some());
|
||||
doc.project.product.display_name = "변경된 제품".into();
|
||||
doc.local
|
||||
.sources
|
||||
.insert("appPayload".into(), "payload".into());
|
||||
doc.local.developer_certificate_thumbprint = Some("A".repeat(40));
|
||||
let saved = infra::save_project(&doc).unwrap();
|
||||
assert_eq!(saved.local.sources["appPayload"], "payload");
|
||||
assert_eq!(
|
||||
saved.local.developer_certificate_thumbprint,
|
||||
doc.local.developer_certificate_thumbprint
|
||||
);
|
||||
assert_eq!(
|
||||
saved.project.formats.msix.identity.name,
|
||||
doc.project.formats.msix.identity.name
|
||||
);
|
||||
assert_eq!(
|
||||
saved.project.formats.msix.identity.publisher,
|
||||
doc.project.formats.msix.identity.publisher
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(infra::appended_path(&doc.path, ".bak")).unwrap(),
|
||||
original
|
||||
);
|
||||
let local_path = infra::local_config_path(&doc.path);
|
||||
assert!(local_path.ends_with("한글 프로젝트.isproj.local.json"));
|
||||
let local_bytes = fs::read(&local_path).unwrap();
|
||||
infra::save_project(&saved).unwrap();
|
||||
assert_eq!(
|
||||
fs::read(infra::appended_path(&local_path, ".bak")).unwrap(),
|
||||
local_bytes
|
||||
);
|
||||
assert!(!temp.path().join("한글 프로젝트.json.bak").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_and_duplicate_extensions_are_rejected_even_with_valid_contents() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let doc = infra::create_project(&temp.path().join("Sample.ISPROJ")).unwrap();
|
||||
assert!(infra::open_project(&doc.path).is_ok());
|
||||
for name in [
|
||||
"legacy.json",
|
||||
"bad.txt",
|
||||
"duplicate.isproj.isproj",
|
||||
"no-extension",
|
||||
] {
|
||||
let path = temp.path().join(name);
|
||||
fs::copy(&doc.path, &path).unwrap();
|
||||
assert!(infra::open_project(&path)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("UnsupportedProjectExtension"));
|
||||
if name != "no-extension" {
|
||||
assert!(infra::create_project(&path)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("UnsupportedProjectExtension"));
|
||||
}
|
||||
}
|
||||
assert!(infra::create_project(&doc.path).is_err());
|
||||
fs::write(temp.path().join("broken.isproj"), "not json").unwrap();
|
||||
assert!(infra::open_project(&temp.path().join("broken.isproj")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_legacy_local_fallback_and_no_overwrite_of_orphan_local_settings() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
fs::write(
|
||||
temp.path().join("package-project.local.json"),
|
||||
r#"{"sources":{"legacy":"do-not-load"},"makeAppx":null}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let doc = infra::create_project(&temp.path().join("fresh.isproj")).unwrap();
|
||||
assert!(doc.local.sources.is_empty());
|
||||
let path = temp.path().join("orphan.isproj");
|
||||
fs::write(infra::local_config_path(&path), "keep").unwrap();
|
||||
assert!(infra::create_project(&path)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("LocalConfigAlreadyExists"));
|
||||
assert!(!path.exists());
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[test]
|
||||
fn local_revision_conflicts_and_failed_backup_preserve_both_files() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let doc = infra::create_project(&temp.path().join("sample.isproj")).unwrap();
|
||||
let mut doc = infra::save_project(&doc).unwrap();
|
||||
let local_path = infra::local_config_path(&doc.path);
|
||||
let original = fs::read(&doc.path).unwrap();
|
||||
let local = fs::read(&local_path).unwrap();
|
||||
fs::write(&local_path, b"{\"sources\":{},\"makeAppx\":null}\n").unwrap();
|
||||
assert!(infra::save_project(&doc)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("RevisionConflict"));
|
||||
fs::write(&local_path, &local).unwrap();
|
||||
fs::create_dir(infra::appended_path(&local_path, ".bak")).unwrap();
|
||||
doc.project.product.display_name = "must not commit".into();
|
||||
doc.local
|
||||
.sources
|
||||
.insert("appPayload".into(), "changed".into());
|
||||
assert!(infra::save_project(&doc).is_err());
|
||||
assert_eq!(fs::read(&doc.path).unwrap(), original);
|
||||
assert_eq!(fs::read(&local_path).unwrap(), local);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn failed_project_replacement_restores_previous_local_settings() {
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let doc = infra::create_project(&temp.path().join("locked.isproj")).unwrap();
|
||||
let mut doc = infra::save_project(&doc).unwrap();
|
||||
let local_path = infra::local_config_path(&doc.path);
|
||||
let project_before = fs::read(&doc.path).unwrap();
|
||||
let local_before = fs::read(&local_path).unwrap();
|
||||
// Permit reading and writing, but hold back FILE_SHARE_DELETE so replacement fails.
|
||||
let _held = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.share_mode(3)
|
||||
.open(&doc.path)
|
||||
.unwrap();
|
||||
doc.project.product.display_name = "must roll back".into();
|
||||
doc.local
|
||||
.sources
|
||||
.insert("appPayload".into(), "changed".into());
|
||||
assert!(infra::save_project(&doc).is_err());
|
||||
assert_eq!(fs::read(&doc.path).unwrap(), project_before);
|
||||
assert_eq!(fs::read(local_path).unwrap(), local_before);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ fn png(path: &Path, size: u32) {
|
||||
}
|
||||
fn fixture() -> (tempfile::TempDir, PathBuf) {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let path = temp.path().join("package-project.json");
|
||||
let path = temp.path().join("InstallerProject.isproj");
|
||||
let mut doc = infra::create_project(&path).unwrap();
|
||||
let payload = temp.path().join("payload");
|
||||
fs::create_dir_all(payload.join("Assets")).unwrap();
|
||||
|
||||
File renamed without changes.
@@ -22,13 +22,13 @@ foreach ($name in @('FixtureAlpha', 'FixtureBeta')) {
|
||||
} finally { $env:FIXTURE_NAME = $previousName }
|
||||
Copy-Item -LiteralPath (Join-Path $sourceRoot 'apps/desktop/src-tauri/icons/Square44x44Logo.png') -Destination (Join-Path $assetRoot 'Logo44.png')
|
||||
Copy-Item -LiteralPath (Join-Path $sourceRoot 'apps/desktop/src-tauri/icons/Square150x150Logo.png') -Destination (Join-Path $assetRoot 'Logo150.png')
|
||||
$project = Get-Content -Raw -Encoding UTF8 (Join-Path $sourceRoot 'fixtures/package-project.json') | ConvertFrom-Json
|
||||
$project = Get-Content -Raw -Encoding UTF8 (Join-Path $sourceRoot 'fixtures/InstallerProject.isproj') | ConvertFrom-Json
|
||||
$project.projectId = $name
|
||||
$project.product.displayName = $name
|
||||
$project.formats.msix.identity.name = "ExamplePublisher.$name"
|
||||
$project.formats.msix.identity.publisher = 'CN=Example Publisher'
|
||||
[IO.File]::WriteAllText((Join-Path $projectRoot 'package-project.json'), ($project | ConvertTo-Json -Depth 30), $utf8)
|
||||
[IO.File]::WriteAllText((Join-Path $projectRoot 'InstallerProject.isproj'), ($project | ConvertTo-Json -Depth 30), $utf8)
|
||||
$local = @{ sources = @{ appPayload = 'payload' }; makeAppx = $sdkTool }
|
||||
[IO.File]::WriteAllText((Join-Path $projectRoot 'package-project.local.json'), ($local | ConvertTo-Json -Depth 10), $utf8)
|
||||
Write-Output (Join-Path $projectRoot 'package-project.json')
|
||||
[IO.File]::WriteAllText((Join-Path $projectRoot 'InstallerProject.isproj.local.json'), ($local | ConvertTo-Json -Depth 10), $utf8)
|
||||
Write-Output (Join-Path $projectRoot 'InstallerProject.isproj')
|
||||
}
|
||||
@@ -179,8 +179,8 @@ def main():
|
||||
"capabilities": ["runFullTrust"], "extensions": [], "dependencies": []}},
|
||||
"targets": [{"id": "msix-direct", "format": "msix", "delivery": "package", "distribution": "direct",
|
||||
"signingProfileRef": None}]}
|
||||
write_json(out / "package-project.json", project)
|
||||
write_json(out / "package-project.local.json", {"sources": local_sources, "makeAppx": str(args.makeappx.resolve())})
|
||||
write_json(out / "InstallerProject.isproj", project)
|
||||
write_json(out / "InstallerProject.isproj.local.json", {"sources": local_sources, "makeAppx": str(args.makeappx.resolve())})
|
||||
write_json(out / "reports/file-mapping.json", mappings)
|
||||
with (out / "reports/file-mapping.csv").open("w", encoding="utf-8-sig", newline="") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=list(mappings[0])); writer.writeheader(); writer.writerows(mappings)
|
||||
|
||||
Reference in new issue
Block a user