From 83db959d79d8404e98b0bc86fa007cbe8ffbbbb9 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 30 Oct 2026 13:49:59 -0300 Subject: [PATCH 1/3] m4: record the C31 Dokany check, audit the M4 row, implement webdav-tune C31 (first week of M4): dumpbin /dependents on dokan2.dll from the pinned v2.3.1.1000 Dokan_x86, Dokan_x64 and Dokan_ARM64 MSIs. Every architecture imports KERNEL32, USER32, ADVAPI32 and SHELL32 only, with no VC++ runtime, so D32 selects the official per-arch MSIs and the VC++ redistributable is not a prerequisite for GPO/Intune. Package and DLL hashes are recorded in docs/dev/m4-notes.md. docs/dev/m4-notes.md also audits the M4 milestone row requirement by requirement (done, partial, missing or blocked, with evidence). webdav-tune (C28) was a silent no-op. --apply now sets the WebClient FileSizeLimitInBytes (0xFFFFFFFF) and FileAttributesLimitInBytes (100000000) after recording the previous values once in service\state\webdav-tune.ini. --revert restores them (deleting values that were absent) and removes the record. BasicAuthLevel is never touched. The hook fails closed on a value of another type or a damaged record. Tests: five new cases in tests/apps/test_hooks.c run the real hook through sd-test-host-fake.exe, redirected to an HKCU key and a temporary ProgramData folder. Gate: tools/ci-local.ps1 passes on x64-modern, x64-xp, x86-ia32, x86-sse2 and x64-analysis (dokany.mount skipped under WOW64 on the x86 presets, as expected). --- apps/shelldrive-host/installer_hooks.c | 332 ++++++++++++++++++++++- docs/dev/m4-notes.md | 117 ++++++++ tests/apps/CMakeLists.txt | 2 +- tests/apps/main.c | 10 + tests/apps/test_hooks.c | 353 +++++++++++++++++++++++++ 5 files changed, 810 insertions(+), 4 deletions(-) create mode 100644 tests/apps/test_hooks.c diff --git a/apps/shelldrive-host/installer_hooks.c b/apps/shelldrive-host/installer_hooks.c index f79bb5e..80f3646 100644 --- a/apps/shelldrive-host/installer_hooks.c +++ b/apps/shelldrive-host/installer_hooks.c @@ -2,9 +2,10 @@ * * Hidden verbs run by the installers (the setup engine per user, MSI deferred actions per machine). Each is * idempotent. signal-shutdown is implemented here for the current account: it sets the shutdown events of - * the agents of this user and session (04 §6.9 step 1). The other hooks are part of M5 (the service and - * the Explorer/WebClient integration); they log that they are not implemented and succeed, so that an - * installer run is not blocked by a hook that has nothing to do yet. */ + * the agents of this user and session (04 §6.9 step 1). webdav-tune sets the two WebClient redirector limits + * and records the values it replaced (C28). programdata-secure and user-cleanup are the other implemented + * hooks. modern-menu is part of M5/the sparse package (D27); it logs that it is not implemented and succeeds, + * so that an installer run is not blocked by a hook that has nothing to do yet. */ #include #include #include @@ -15,6 +16,7 @@ #include "svc_repair.h" #include "explorer_data.h" +#include "sd_config/sd_ini.h" #define SD_HOOK_AGENT_EVENT L"Global\\com.librestatic.shelldrive.agent.shutdown." @@ -96,6 +98,324 @@ static int signal_own_agents(void) return rc; } +/* ---- webdav-tune (C28; 04 §8.3, 02 §7.1) ---------------------------------------------------------------- */ + +#define SD_WEBDAV_PARAMS_KEY L"SYSTEM\\CurrentControlSet\\Services\\WebClient\\Parameters" +#define SD_WEBDAV_SIZE_NAME L"FileSizeLimitInBytes" +#define SD_WEBDAV_ATTR_NAME L"FileAttributesLimitInBytes" +#define SD_WEBDAV_SIZE_VALUE 0xFFFFFFFFu +#define SD_WEBDAV_ATTR_VALUE 100000000u +#define SD_WEBDAV_TUNE_FILE L"webdav-tune.ini" +#define SD_WEBDAV_SECTION "webdav-tune" +#define SD_WEBDAV_SIZE_KEY "file_size_limit_in_bytes" +#define SD_WEBDAV_ATTR_KEY "file_attributes_limit_in_bytes" +#define SD_WEBDAV_ABSENT "absent" +#define SD_WEBDAV_STATE_MAX 4096 + +typedef struct webdav_original { + DWORD value; + int present; +} webdav_original; + +/* Creates every component of dir that is missing. Drive paths only; the callers build them from a known folder. */ +static int ensure_dirs(const wchar_t *dir) +{ + wchar_t tmp[MAX_PATH]; + size_t i; + + if (FAILED(StringCchCopyW(tmp, SD_COUNTOF(tmp), dir))) + return 1; + for (i = 3; tmp[i] != L'\0'; i++) { + if (tmp[i] == L'\\') { + tmp[i] = L'\0'; + (void)CreateDirectoryW(tmp, NULL); + tmp[i] = L'\\'; + } + } + (void)CreateDirectoryW(tmp, NULL); + return GetFileAttributesW(tmp) == INVALID_FILE_ATTRIBUTES ? 1 : 0; +} + +/* %ProgramData% (the test build may redirect it, like the other host test hooks). */ +static int programdata_dir(wchar_t *out, size_t cch) +{ +#ifdef SD_HOST_TEST_HOOKS + DWORD n = GetEnvironmentVariableW(L"SHELLDRIVE_TEST_PROGRAMDATA", out, (DWORD)cch); + if (n > 0 && (size_t)n < cch) + return 0; +#else + (void)cch; +#endif + return FAILED(SHGetFolderPathW(NULL, CSIDL_COMMON_APPDATA, NULL, SHGFP_TYPE_CURRENT, out)) ? 1 : 0; +} + +/* \LibreStatic\ShellDrive\service\state\webdav-tune.ini (03 §6.1). */ +static int webdav_state_path(wchar_t *out, size_t cch) +{ + wchar_t base[MAX_PATH]; + wchar_t dir[MAX_PATH]; + + if (programdata_dir(base, SD_COUNTOF(base)) != 0) + return 1; + if (FAILED(StringCchPrintfW(dir, SD_COUNTOF(dir), L"%s\\LibreStatic\\ShellDrive\\service\\state", base))) + return 1; + if (ensure_dirs(dir) != 0) + return 1; + return FAILED(StringCchPrintfW(out, cch, L"%s\\%s", dir, SD_WEBDAV_TUNE_FILE)) ? 1 : 0; +} + +/* The parameters key, or the HKCU key of the test build (never the real user's in tests). */ +static void webdav_params_key(HKEY *root, const wchar_t **key) +{ + *root = HKEY_LOCAL_MACHINE; + *key = SD_WEBDAV_PARAMS_KEY; +#ifdef SD_HOST_TEST_HOOKS + { + static wchar_t test[MAX_PATH]; + DWORD n = GetEnvironmentVariableW(L"SHELLDRIVE_TEST_WEBDAV_PARAMS", test, SD_COUNTOF(test)); + if (n > 0 && (size_t)n < SD_COUNTOF(test)) { + *root = HKEY_CURRENT_USER; + *key = test; + } + } +#endif +} + +/* Reads one DWORD value. A value of another type is an error: it is never replaced or deleted. */ +static int webdav_read_dword(HKEY key, const wchar_t *name, webdav_original *v) +{ + DWORD type = 0; + DWORD size = sizeof(DWORD); + DWORD data = 0; + LONG rc; + + memset(v, 0, sizeof(*v)); + rc = RegQueryValueExW(key, name, NULL, &type, (BYTE *)&data, &size); + if (rc == ERROR_FILE_NOT_FOUND) + return 0; + if (rc != ERROR_SUCCESS || type != REG_DWORD || size != sizeof(DWORD)) + return 1; + v->value = data; + v->present = 1; + return 0; +} + +static int webdav_write_dword(HKEY key, const wchar_t *name, DWORD value) +{ + return RegSetValueExW(key, name, 0, REG_DWORD, (const BYTE *)&value, sizeof(value)) == ERROR_SUCCESS ? 0 : 1; +} + +static int webdav_restore_dword(HKEY key, const wchar_t *name, const webdav_original *v) +{ + LONG rc; + + if (v->present) + return webdav_write_dword(key, name, v->value); + rc = RegDeleteValueW(key, name); + return (rc == ERROR_SUCCESS || rc == ERROR_FILE_NOT_FOUND) ? 0 : 1; +} + +static int webdav_parse_original(const char *text, webdav_original *v) +{ + uint32_t u = 0; + + memset(v, 0, sizeof(*v)); + if (text == NULL) + return 1; + if (strcmp(text, SD_WEBDAV_ABSENT) == 0) + return 0; + if (!sd_ini_parse_u32(text, &u)) + return 1; + v->value = (DWORD)u; + v->present = 1; + return 0; +} + +/* Reads the recorded originals. *exists is 0 when the file is absent (nothing was tuned); a damaged file is an error, + so a repeat apply never overwrites them and a revert never guesses. */ +static int webdav_read_state(const wchar_t *path, int *exists, webdav_original *size, webdav_original *attr) +{ + HANDLE f; + char buf[SD_WEBDAV_STATE_MAX]; + DWORD got = 0; + sd_ini *ini = NULL; + uint32_t flags = 0; + int rc = 1; + + *exists = 0; + memset(size, 0, sizeof(*size)); + memset(attr, 0, sizeof(*attr)); + f = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (f == INVALID_HANDLE_VALUE) + return GetLastError() == ERROR_FILE_NOT_FOUND ? 0 : 1; + *exists = 1; + if (!ReadFile(f, buf, (DWORD)sizeof(buf), &got, NULL) || got == 0 || got >= (DWORD)sizeof(buf)) + goto out; + if (sd_ini_parse(buf, got, &ini, &flags) != SD_OK) + goto out; + if (webdav_parse_original(sd_ini_get(ini, SD_WEBDAV_SECTION, SD_WEBDAV_SIZE_KEY), size) != 0 || + webdav_parse_original(sd_ini_get(ini, SD_WEBDAV_SECTION, SD_WEBDAV_ATTR_KEY), attr) != 0) + goto out; + rc = 0; +out: + if (ini != NULL) + sd_ini_free(ini); + CloseHandle(f); + return rc; +} + +/* Writes the originals before anything changes (tmp + replace, so a crash leaves either the old file or none). */ +static int webdav_write_state(const wchar_t *path, const webdav_original *size, const webdav_original *attr) +{ + sd_ini *ini = NULL; + sd_wr w; + wchar_t tmp[MAX_PATH]; + HANDLE f; + DWORD put = 0; + char text[16]; + int rc = 1; + + sd_wr_init(&w, SD_WEBDAV_STATE_MAX); + if (sd_ini_new(&ini) != SD_OK) { + sd_wr_free(&w); + return 1; + } + if (size->present && _snprintf_s(text, SD_COUNTOF(text), _TRUNCATE, "%lu", (unsigned long)size->value) < 0) + goto out; + if (sd_ini_set(ini, SD_WEBDAV_SECTION, SD_WEBDAV_SIZE_KEY, size->present ? text : SD_WEBDAV_ABSENT) != SD_OK) + goto out; + if (attr->present && _snprintf_s(text, SD_COUNTOF(text), _TRUNCATE, "%lu", (unsigned long)attr->value) < 0) + goto out; + if (sd_ini_set(ini, SD_WEBDAV_SECTION, SD_WEBDAV_ATTR_KEY, attr->present ? text : SD_WEBDAV_ABSENT) != SD_OK) + goto out; + if (sd_ini_serialize(ini, &w) != SD_OK || !sd_wr_ok(&w)) + goto out; + if (FAILED(StringCchPrintfW(tmp, SD_COUNTOF(tmp), L"%s.tmp", path))) + goto out; + f = CreateFileW(tmp, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (f == INVALID_HANDLE_VALUE) + goto out; + if (!WriteFile(f, w.p, (DWORD)w.len, &put, NULL) || put != (DWORD)w.len || !FlushFileBuffers(f)) { + CloseHandle(f); + (void)DeleteFileW(tmp); + goto out; + } + CloseHandle(f); + if (!MoveFileExW(tmp, path, MOVEFILE_REPLACE_EXISTING)) { + (void)DeleteFileW(tmp); + goto out; + } + rc = 0; +out: + sd_wr_free(&w); + sd_ini_free(ini); + return rc; +} + +/* Sets FileSizeLimitInBytes=0xFFFFFFFF and FileAttributesLimitInBytes=100000000, recording the previous values once. + Idempotent: a repair run or a second install keeps the originals of the first run. BasicAuthLevel is never read or + written (A2, C28). */ +static int webdav_tune_apply(void) +{ + HKEY root; + const wchar_t *keypath; + HKEY key = NULL; + DWORD disp = 0; + webdav_original cur_size, cur_attr, old_size, old_attr; + wchar_t state[MAX_PATH]; + int have = 0; + LONG rc; + int result = 1; + + webdav_params_key(&root, &keypath); + if (webdav_state_path(state, SD_COUNTOF(state)) != 0) { + wprintf(L"webdav-tune: the machine state folder is not available\n"); + return 1; + } + rc = RegCreateKeyExW(root, keypath, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_QUERY_VALUE | KEY_SET_VALUE, NULL, &key, + &disp); + if (rc != ERROR_SUCCESS) { + wprintf(L"webdav-tune: the WebClient parameters key cannot be opened (error %ld)\n", (long)rc); + return 1; + } + if (webdav_read_dword(key, SD_WEBDAV_SIZE_NAME, &cur_size) != 0 || + webdav_read_dword(key, SD_WEBDAV_ATTR_NAME, &cur_attr) != 0) { + wprintf(L"webdav-tune: a WebClient value has an unexpected type; nothing was changed\n"); + goto out; + } + if (webdav_read_state(state, &have, &old_size, &old_attr) != 0) { + wprintf(L"webdav-tune: the recorded originals are unreadable; nothing was changed\n"); + goto out; + } + if (!have) { + old_size = cur_size; + old_attr = cur_attr; + if (webdav_write_state(state, &old_size, &old_attr) != 0) { + wprintf(L"webdav-tune: the recorded originals could not be written; nothing was changed\n"); + goto out; + } + } + if (webdav_write_dword(key, SD_WEBDAV_SIZE_NAME, SD_WEBDAV_SIZE_VALUE) != 0 || + webdav_write_dword(key, SD_WEBDAV_ATTR_NAME, SD_WEBDAV_ATTR_VALUE) != 0) { + wprintf(L"webdav-tune: the WebClient values could not be written\n"); + goto out; + } + wprintf(L"webdav-tune: the WebClient limits are set (originals recorded in %s)\n", state); + result = 0; +out: + RegCloseKey(key); + return result; +} + +/* Puts the recorded values back and removes the record. Without a record there is nothing to undo (a hook that never + applied, or an older install): success. */ +static int webdav_tune_revert(void) +{ + HKEY root; + const wchar_t *keypath; + HKEY key = NULL; + webdav_original old_size, old_attr; + wchar_t state[MAX_PATH]; + int exists = 0; + LONG rc; + + webdav_params_key(&root, &keypath); + if (webdav_state_path(state, SD_COUNTOF(state)) != 0) { + wprintf(L"webdav-tune: the machine state folder is not available\n"); + return 1; + } + if (webdav_read_state(state, &exists, &old_size, &old_attr) != 0) { + wprintf(L"webdav-tune: the recorded originals are unreadable; nothing was restored\n"); + return 1; + } + if (!exists) { + wprintf(L"webdav-tune: no originals recorded; nothing to restore\n"); + return 0; + } + rc = RegOpenKeyExW(root, keypath, 0, KEY_QUERY_VALUE | KEY_SET_VALUE, &key); + if (rc == ERROR_FILE_NOT_FOUND) { + (void)DeleteFileW(state); + return 0; + } + if (rc != ERROR_SUCCESS) { + wprintf(L"webdav-tune: the WebClient parameters key cannot be opened (error %ld)\n", (long)rc); + return 1; + } + if (webdav_restore_dword(key, SD_WEBDAV_SIZE_NAME, &old_size) != 0 || + webdav_restore_dword(key, SD_WEBDAV_ATTR_NAME, &old_attr) != 0) { + wprintf(L"webdav-tune: the originals could not be restored\n"); + RegCloseKey(key); + return 1; + } + RegCloseKey(key); + if (!DeleteFileW(state) && GetLastError() != ERROR_FILE_NOT_FOUND) { + wprintf(L"webdav-tune: the originals are back but the record could not be removed\n"); + return 1; + } + wprintf(L"webdav-tune: the original WebClient limits are back\n"); + return 0; +} + /* argv as given to wmain: argv[1] == "--installer-hook", argv[2] = name, then --apply or --revert. */ int sd_host_installer_hook(int argc, wchar_t **argv) { @@ -147,6 +467,12 @@ int sd_host_installer_hook(int argc, wchar_t **argv) return 0; } + if (wcscmp(name, L"webdav-tune") == 0) { + if (!apply) + return webdav_tune_revert(); + return webdav_tune_apply(); + } + wprintf(L"installer hook %s: not yet implemented (M5)\n", name); return 0; } diff --git a/docs/dev/m4-notes.md b/docs/dev/m4-notes.md index 3c91bb2..cd674db 100644 --- a/docs/dev/m4-notes.md +++ b/docs/dev/m4-notes.md @@ -730,3 +730,120 @@ install; the 32-bit shell registration; empty keys left by the per-user removal) never depends on the log). The two tests that caught it (`setup_elev_parent_malformed_block_writes_nothing`, `setup_elev_parent_oversized_block_writes_nothing`) were unregistered before this branch. - Also fixed: a use of `run_machine_elevated` before its definition, and an analysis warning (NULL `out`) in `key_sz`. + +## C31: Dokany VC++ dependency check (first week of M4) + +The risk table (`docs/design/00-integration-review.md`, C31) asks for `dumpbin /dependents dokan2.dll` on every +architecture of the Dokany bundle we embed, to choose between the official per-arch MSIs (D32 form a) and the upstream +`DokanSetup.exe` bundle (form b). Run on 2026-10-30. + +**Method.** The three installers were downloaded from the pinned release +`v2.3.1.1000` (`https://github.com/dokan-dev/dokany/releases/tag/v2.3.1.1000`); their sizes and SHA-256 match the +GitHub asset `digest` values and the facts recorded in 04 §7.1. Each package was expanded with +`msiexec /a TARGETDIR=` (an admin image needs no elevation and runs on the x64 host), and the +installed copy of the DLL (`Windows\System32\dokan2.dll` of each image) was read with +`dumpbin /dependents` (MSVC 14.44, `dumpbin` 14.44.35229.0) on a Windows 11 IoT Enterprise LTSC 2024 x64 host. + +| Architecture | Package (size, SHA-256) | `dokan2.dll` SHA-256 | Imported modules | +|---|---|---|---| +| x86 | `Dokan_x86.msi`, 5,128,192 B, `c833fd51f67c44a467eeade419b8934033445ee847119f82cbef1fddc84ef618` | `516da99d541961260888631c8cad3869a9e4e5021904560aa6534ac7ae549c75` | KERNEL32, USER32, ADVAPI32, SHELL32 | +| x64 | `Dokan_x64.msi`, 9,269,248 B, `69ff8cb37bfec3a75921c85ffd1c6370b50a9ec4ecef2cf3a009d488dcbf5465` | `75600aba867acbdfdb85fcd142b524da769bdc611b855a760aeb0c6e2eaae17a` | KERNEL32, USER32, ADVAPI32, SHELL32 | +| ARM64 | `Dokan_ARM64.msi`, 5,128,192 B, `f812a5bf11c15bc3910a1f0f53830ff771ceb3c80458f10686b0f45b0db91d2e` | `52206ef6a5948f29bfb2d217fbb7d3a961072bf98aab520951abd306683db96a` | KERNEL32, USER32, ADVAPI32, SHELL32 | + +**Conclusion: no VC++ runtime dependency on any architecture.** `dokan2.dll` 2.3.1 imports no +`vcruntime140*.dll`, no `msvcp140*.dll` and no other non-OS module. D32 therefore selects form (a): + +- the offline setup EXEs embed the unmodified official `Dokan_.msi` (`Dokan_x86.msi` for `x86-ia32` and + `x86-sse2`, `Dokan_x64.msi` for `x64-xp` and `x64-modern`, `Dokan_ARM64.msi` for `arm64`); +- the MSI deployments for GPO/Intune use the same upstream packages and the VC++ 2015–2022 redistributable is **not** + a prerequisite (04 §7.1's "otherwise" branch does not apply); +- the upstream `DokanSetup.exe` bundle is not needed and is not embedded; +- the ARM32 flavour chains no Dokany package: there is no ARM32 Dokany installer (04 §7.1). The ARM32 MSI (D31) + installs CLI + agent + WebDAV only. + +`installer/dokany/dokany.lock` (not yet in the tree) records `vcruntime = none` for the three architectures, with the +package sizes and SHA-256 above, the DLL hashes and this tool output. The [lab-verify] signer-subject and +service-name items of 04 §7.1/§7.3 stay open. + +## M4 row audit (2026-10-30) + +Requirement by requirement against `docs/design/00-integration-review.md` §6 (M4 row) and `decisions.md` (which +overrides the review's recommendation column where they differ). Status words: **Done** = implemented and checked; +**Partial** = some of it exists; **Missing** = not in the tree; **Blocked** = needs the lab, the release keys, or +another milestone. Evidence paths are relative to the repository root. + +| Requirement | Status | Evidence and remaining work | +|---|---|---| +| Setup stub from the `x86-ia32` preset, CAB payloads | **Done** | `apps/shelldrive-setup/` (`shelldrive-setup-stub`, `shelldrive-setup`), `cmake/SdPayload.cmake` (FDI CAB via `cabinet.dll`, fixed member times), `tests/setup` (`sd-test-engine`, 54 tests), import and ISA audits | +| Per-user, per-machine and portable | **Done in code; lab pending** | `setup_install.c`, `setup_elevate.c` (two stages, pipe relay, per-machine uninstall), `tests/setup/test_elevate.c`. Lab: UAC/Run As, msiexec, 3010 (04 §11.4) | +| Six flavours and stub selection by CPU and OS (D4, D28) | **Partial** | Presets for all six (`CMakePresets.json`); `sd_setup_select_flavour` covers x86 SSE2, x64 by OS version and the ARM paths. Stub and payload targets exist only in the `x86-ia32`, `arm64` and `arm32` presets: **no preset builds an x64 stub or payload**, and the universal EXE that carries `PAYLOAD_X86_SSE2`, `PAYLOAD_X64_XP` and `PAYLOAD_X64_MODERN` is release-pipeline work (`tools/sd-payload` attaches one flavour per run). The `x86-ia32` EXE built here therefore selects `x86-sse2` on an SSE2 host and reports `payload_missing` until the pipeline assembles the universal EXE. ARM stubs are cross-built and cannot run here (04 §11.4) | +| WiX MSIs per flavour, incl. ARM32 (D31 best effort per D5) | **Partial / Missing (ARM)** | x86 pair and x64 pair build; `msi..tables`, `.admin` (admin image only) and `.wixui` pass. `packaging/msi/CMakeLists.txt` refuses `SD_ARM64` and has no ARM32 row. `decisions.md` D31 requires the ARM32 MSI (the review table's "(a) no" is superseded). Both need a cross-tree `msi` target with host-built generator tools | +| Language transforms (C43) | **Partial** | `sd-msigen` LCID table (es under 3082/11274/1034/2058/21514/…, pt-BR under 1046/2070), `sd-msiembed`, checked by `msi..tables`. **The setup EXE never passes `TRANSFORMS` explicitly** (the string appears only in `sd-msiembed`), so a `/lang=` choice that differs from the OS language gets the OS transform on a per-machine install | +| Dokany chaining (C31, D32) | **Partial** | `setup_dokany.c`: decision, PE import reader, command lines and run gate, 5 tests. Nothing calls `sd_setup_dokany_run`; no `DOKANY_` payload member, no `installer/dokany/dokany.lock`, no post-install `--selftest` probe. C31 fixes the form: official per-arch MSIs | +| `webdav-tune` and the other installer hooks | **Partial → webdav-tune closed here** | `signal-shutdown`, `user-cleanup` and `programdata-secure` implemented; `modern-menu` is a deliberate no-op until the sparse package exists (D27); `webdav-tune` was a silent no-op and is implemented in this change (below) | +| Upgrade with running processes (C3, C4) | **Partial** | C3 step 1 done and tested: the agent creates the manual-reset `Global\…\agent.shutdown..` event with the `user, SY, BA` DACL, waits on it (`agent_shutdown_event_stops_agent`), setup signals it and waits 15 s (`setup_host.c`, `setup_install.c`), locked files are renamed aside. Missing: the Restart Manager step 2 and C4's versioned `shellext\\` directory (the fixed path plus rename-aside is used). Lab pending | +| Signed manifest | **Done; blocked on the release roots** | `src/sd_update` (parse, chain, rollback, freshness, tests). `k_trust` in `update_ipc.c` is empty, so every verification fails closed until `installer/keys` exists | +| Static HTTP mirror (D30) | **Partial; blocked on the domain** | `manifest_alt` fallback after a network error and the redirect allow list exist; the default URL is the GitHub release URL only. The mirror URL is an operational choice (D30) | +| Updater UI and rollback | **Done** | `apps/shelldrive-ui/ui_update.c` (Settings group, consent dialog, tray balloon) over `update.check` / `update.apply`; `update_engine.c` apply with `.rollback`, `--selftest` and byte-for-byte restore, covered by the update tests | +| ADMX/ADML for every 03 §3.5 policy | **Done** | 21 policies in `res/policy/policies.txt`; `tools/sd-admxgen` emits the ADMX, six ADML files, `sd_policy_ids.h`/`sd_policy_table.h`; `policy.*`, `unit.admxgen`, `unit.policy`, `l10n.policy_catalogs_complete` | +| Sparse MSIX build switch (D27) | **Missing** | No `packaging/msix`, no `SD_SPARSE_MSIX` option; `modern-menu` stays a no-op. D27 defers *shipping* until the certificate exists, not the build switch | +| Reproducible-build check | **Done; notes placeholder** | `tools/check-repro.ps1` and `repro.*` (excluded from the default gate). The "Results" section above still says `RESULTS_PLACEHOLDER` | +| Service installed and enabled by default per machine (D6) | **Blocked (M5)** | `SD_MSI_WITH_SERVICE=0` until the service binary exists; the `ServiceInstall`/`ServiceControl` authoring is behind the define | +| `ENABLELINKEDCONNECTIONS` (D24) | **Done** | MSI property default 0; `AppSearch` ownership rules so an admin's pre-existing value is neither written nor removed; `msi..tables` | +| `DEFAULTPROFILES` (C9) | **Done (authored); install lab pending** | `MoveFile` custom table and `C_DefaultProfiles` in `packaging/msi/ShellDrive.wxs` | +| Signed payload tables (TM-R38) | **Partial; blocked on the keys** | Verifier, domain separation, resource check and 14 tests (`tests/setup/test_payload_sig.c`). `setup_keyring.inc` holds a revoked placeholder; `installer/keys`, the generated keyring and the package step's `--sig` input are release work | +| Release pipeline and two offline signing sessions (D42) | **Partial** | `.github/workflows/release.yml` skeleton stops at `package`/`finalize`; `sd-release session/sign/verify/gates` exist with tests; `resign` and `installer/` (keys, `release-gates.txt`, `dokany.lock`) are missing; the workflow lint reports the missing `CODEOWNERS` | +| Security-test support TST-29/31/32 | **Partial** | `tools/check-workflows.ps1` (TST-32) runs in `ci.yml` and `nightly.yml`; `tools/sd-msilang --dump-sequences` (TST-29) does not exist; the TST-27/31 lab checks were not run | + +Two earlier open items in this file are superseded: open item 6 of the setup section (the agent shutdown event) is +implemented in `src/sd_ipc/ipc_common.c` and `agent.c` and covered by `agent_shutdown_event_stops_agent`; and the +"21 policies" figure is current. + +### Gap closed in this change: `webdav-tune` (C28) + +**Why this one.** Of the M4-row gaps that are neither blocked on the release keys/domain nor on the lab, `webdav-tune` +is the one that silently degrades a shipped per-machine install: without it the WebClient redirector keeps its 50 MB +file and 1 MB attribute limits, so the WebDAV backend fails on large files and large folders while the documentation +promises the tuning. Dokany chaining, the universal multi-flavour EXE and the ARM MSIs are higher-impact but need +upstream packaging and the lab; the sparse MSIX switch needs the certificate that D27 makes a condition. + +**Implementation** (`apps/shelldrive-host/installer_hooks.c`): + +- `--apply` sets `HKLM\SYSTEM\CurrentControlSet\Services\WebClient\Parameters\FileSizeLimitInBytes = 0xFFFFFFFF` and + `FileAttributesLimitInBytes = 100000000` (02 §7.1, C28). No other value is read or written: in particular + `BasicAuthLevel` is never touched (A2, C26). +- The previous values are recorded **before** the first write in + `%ProgramData%\LibreStatic\ShellDrive\service\state\webdav-tune.ini` (03 §6.1) as + `[webdav-tune] file_size_limit_in_bytes = ` and `file_attributes_limit_in_bytes = ...`. The file is + written to a temporary name and renamed. A repair run or a second install finds the record and **does not overwrite + it**, so the values restored later are the ones of the first install. +- `--revert` restores each recorded value (a DWORD, or deletes the value when the record says `absent`) and removes + the record. With no record it succeeds without touching the registry (the hook never applied, or an older install). +- Fail closed: a value of another registry type, an unreadable record or a record with an unknown value refuses the + hook (exit 1) and leaves the registry alone. A damaged record is never replaced by a guess. +- The MSI actions were already authored (`CA_WebDavTune_Apply` deferred, `Impersonate="no"`, after `InstallFiles`, + condition `WEBDAV_TUNING = "1" AND NOT REMOVE~="ALL"`; `CA_WebDavTune_Revert` before `RemoveFiles`, condition + `REMOVE~="ALL" AND NOT UPGRADINGPRODUCTCODE`), so GPO/Intune installs run it as SYSTEM. The per-user setup does not + call it (no HKLM access); 02 §7.1 tells the user to run `shelldrive --installer-hook webdav-tune --apply` elevated. + +**Tests** (`tests/apps/test_hooks.c`, 5 new, registered in `tests/apps/main.c`): the hook runs as a real process +(`sd-test-host-fake.exe --installer-hook webdav-tune ...`) with two test-only overrides under `SD_HOST_TEST_HOOKS`: +`SHELLDRIVE_TEST_WEBDAV_PARAMS` (an HKCU key instead of the HKLM parameters key) and `SHELLDRIVE_TEST_PROGRAMDATA` +(a temporary folder instead of `%ProgramData%`). No real machine value, key or folder is touched. + +1. `hooks_webdav_tune_apply_and_revert_restore_originals`: both limits set, the record written, `BasicAuthLevel` + untouched, originals restored and the record removed by `--revert`. +2. `hooks_webdav_tune_records_absence_and_repeat_apply_keeps_originals`: values absent before the first apply are + deleted on revert; a repeat apply after the values were changed elsewhere keeps the first originals. +3. `hooks_webdav_tune_revert_without_state_is_a_noop`. +4. `hooks_webdav_tune_refuses_a_damaged_state`: both verbs refuse and leave the values alone. +5. `hooks_webdav_tune_refuses_a_value_of_another_type`: a `REG_SZ` value is refused and no record is written. + +The test key is removed with `RegDeleteKeyW` (it holds values only): `RegDeleteTreeW` is not declared for the XP +presets, which is what broke the first gate run on `x64-xp`, `x86-ia32` and `x86-sse2`. + +Gate note for worktrees: the WSL sshd fixture writes `tests/fixtures/wsl-sshd/state/env` inside the checkout that +started it. When sshd on port 2222 was started from another checkout, `start.ps1` reports "already running" and this +worktree has no `state/env`, so `interop.ssh`, `interop.zlib` and `interop.sftp_ssh` exit 77 and `ci-local` fails on +the skip. Run the gate with `SD_SSH_WSL_FIXTURE_DIR` set to the `tests/fixtures/wsl-sshd` folder of the checkout that +owns the running fixture. diff --git a/tests/apps/CMakeLists.txt b/tests/apps/CMakeLists.txt index 9506d14..b14627e 100644 --- a/tests/apps/CMakeLists.txt +++ b/tests/apps/CMakeLists.txt @@ -31,7 +31,7 @@ target_link_libraries(sd-test-cli PRIVATE sd_headers sd_ipc sd_config sd_l10n sd set_target_properties(sd-test-cli PROPERTIES FOLDER "tests") sd_add_unit_test(apps - SOURCES main.c test_cli_args.c test_cli_m3.c test_smoke.c test_agent_m3.c test_diag_units.c "${_cli_dir}/cli_args.c" + SOURCES main.c test_cli_args.c test_cli_m3.c test_smoke.c test_agent_m3.c test_hooks.c test_diag_units.c "${_cli_dir}/cli_args.c" "${_cli_dir}/cli_zip.c" "${_cli_dir}/cli_anon.c" "${_cli_dir}/cli_diag_rules.c" test_agent_events.c "${_host_dir}/agent_events.c" DEPS sd_ipc sd_state sd_config sd_platform sd_base sd_crypto ole32 shell32 advapi32 user32) diff --git a/tests/apps/main.c b/tests/apps/main.c index 683b727..f9e72b3 100644 --- a/tests/apps/main.c +++ b/tests/apps/main.c @@ -59,6 +59,11 @@ SD_TEST_DECL(diag_deny_parse_truncates_at_the_capacity); SD_TEST_DECL(diag_netloc_compare_token_lists); SD_TEST_DECL(cli_parse_location_verbs); SD_TEST_DECL(smoke_location_verbs); +SD_TEST_DECL(hooks_webdav_tune_apply_and_revert_restore_originals); +SD_TEST_DECL(hooks_webdav_tune_records_absence_and_repeat_apply_keeps_originals); +SD_TEST_DECL(hooks_webdav_tune_revert_without_state_is_a_noop); +SD_TEST_DECL(hooks_webdav_tune_refuses_a_damaged_state); +SD_TEST_DECL(hooks_webdav_tune_refuses_a_value_of_another_type); static const sd_test_case k_tests[] = { SD_TEST_ENTRY(cli_split_follows_msvc_rules), @@ -120,6 +125,11 @@ static const sd_test_case k_tests[] = { SD_TEST_ENTRY(diag_netloc_compare_token_lists), SD_TEST_ENTRY(cli_parse_location_verbs), SD_TEST_ENTRY(smoke_location_verbs), + SD_TEST_ENTRY(hooks_webdav_tune_apply_and_revert_restore_originals), + SD_TEST_ENTRY(hooks_webdav_tune_records_absence_and_repeat_apply_keeps_originals), + SD_TEST_ENTRY(hooks_webdav_tune_revert_without_state_is_a_noop), + SD_TEST_ENTRY(hooks_webdav_tune_refuses_a_damaged_state), + SD_TEST_ENTRY(hooks_webdav_tune_refuses_a_value_of_another_type), }; int main(int argc, char **argv) diff --git a/tests/apps/test_hooks.c b/tests/apps/test_hooks.c new file mode 100644 index 0000000..0d34e8e --- /dev/null +++ b/tests/apps/test_hooks.c @@ -0,0 +1,353 @@ +/* Installer hooks over the real hidden verb (sd-test-host-fake.exe --installer-hook ...): webdav-tune (C28, 04 §8.3). + * + * Every test runs the hook in its own process with the test overrides of the host build: + * SHELLDRIVE_TEST_WEBDAV_PARAMS an HKCU key used instead of HKLM\SYSTEM\...\Services\WebClient\Parameters + * SHELLDRIVE_TEST_PROGRAMDATA a temporary folder used instead of %ProgramData% + * so the real machine values and the machine state folder are never touched. The tests check the two limits, the + * recorded originals, the idempotence of a repeat apply, the restoration on --revert, and that a value of another + * type or a damaged record is refused instead of overwritten. */ +#include +#include +#include +#include + +#include "sd_test.h" + +static wchar_t g_dir[MAX_PATH]; +static wchar_t g_key[256]; +static wchar_t g_root[MAX_PATH]; +static unsigned g_n; + +/* Fresh executable folder, HKCU key and %ProgramData% root for this test. */ +static int prepare(void) +{ + wchar_t exe[MAX_PATH]; + wchar_t temp[MAX_PATH]; + LARGE_INTEGER qpc; + DWORD n; + size_t i, slash = 0; + + n = GetModuleFileNameW(NULL, exe, MAX_PATH); + if (n == 0 || n >= MAX_PATH) + return 0; + for (i = 0; exe[i] != L'\0'; i++) + if (exe[i] == L'\\') + slash = i; + exe[slash] = L'\0'; + if (FAILED(StringCchCopyW(g_dir, SD_COUNTOF(g_dir), exe))) + return 0; + QueryPerformanceCounter(&qpc); + g_n++; + if (GetTempPathW(MAX_PATH, temp) == 0) + return 0; + if (FAILED(StringCchPrintfW(g_root, SD_COUNTOF(g_root), L"%ssd-hooks-%lu-%lu-%08lx", temp, + (unsigned long)GetCurrentProcessId(), (unsigned long)g_n, + (unsigned long)qpc.LowPart))) + return 0; + (void)CreateDirectoryW(g_root, NULL); + if (FAILED(StringCchPrintfW(g_key, SD_COUNTOF(g_key), L"Software\\LibreStatic\\ShellDriveTest\\hooks-%lu-%lu-%08lx", + (unsigned long)GetCurrentProcessId(), (unsigned long)g_n, + (unsigned long)qpc.LowPart))) + return 0; + SetEnvironmentVariableW(L"SHELLDRIVE_TEST_PROGRAMDATA", g_root); + SetEnvironmentVariableW(L"SHELLDRIVE_TEST_WEBDAV_PARAMS", g_key); + return 1; +} + +static void delete_tree(const wchar_t *dir) +{ + wchar_t pattern[MAX_PATH]; + WIN32_FIND_DATAW fd; + HANDLE h; + + if (FAILED(StringCchPrintfW(pattern, SD_COUNTOF(pattern), L"%s\\*", dir))) + return; + h = FindFirstFileW(pattern, &fd); + if (h != INVALID_HANDLE_VALUE) { + do { + wchar_t path[MAX_PATH]; + if (wcscmp(fd.cFileName, L".") == 0 || wcscmp(fd.cFileName, L"..") == 0) + continue; + if (FAILED(StringCchPrintfW(path, SD_COUNTOF(path), L"%s\\%s", dir, fd.cFileName))) + continue; + if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + delete_tree(path); + else + (void)DeleteFileW(path); + } while (FindNextFileW(h, &fd)); + FindClose(h); + } + (void)RemoveDirectoryW(dir); +} + +static void cleanup(void) +{ + /* The test key holds values only (no subkeys), so RegDeleteKeyW (XP floor) removes it. */ + (void)RegDeleteKeyW(HKEY_CURRENT_USER, g_key); + (void)RegDeleteKeyW(HKEY_CURRENT_USER, L"Software\\LibreStatic\\ShellDriveTest"); /* only if no other run left a key */ + delete_tree(g_root); + SetEnvironmentVariableW(L"SHELLDRIVE_TEST_PROGRAMDATA", NULL); + SetEnvironmentVariableW(L"SHELLDRIVE_TEST_WEBDAV_PARAMS", NULL); +} + +static int reg_open(HKEY *out) +{ + DWORD disp = 0; + return RegCreateKeyExW(HKEY_CURRENT_USER, g_key, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, out, + &disp) == ERROR_SUCCESS ? 0 : 1; +} + +static int reg_set_dword(const wchar_t *name, DWORD value) +{ + HKEY k; + LONG rc; + + if (reg_open(&k) != 0) + return 0; + rc = RegSetValueExW(k, name, 0, REG_DWORD, (const BYTE *)&value, sizeof(value)); + RegCloseKey(k); + return rc == ERROR_SUCCESS ? 1 : 0; +} + +static int reg_set_sz(const wchar_t *name, const wchar_t *value) +{ + HKEY k; + LONG rc; + DWORD bytes = (DWORD)((wcslen(value) + 1) * sizeof(wchar_t)); + + if (reg_open(&k) != 0) + return 0; + rc = RegSetValueExW(k, name, 0, REG_SZ, (const BYTE *)value, bytes); + RegCloseKey(k); + return rc == ERROR_SUCCESS ? 1 : 0; +} + +/* A value of this test key: 1 and *present when it is a DWORD, 1 and *present = 0 when absent, 0 on another type. */ +static int reg_get_dword(const wchar_t *name, DWORD *value, int *present) +{ + HKEY k; + DWORD type = 0; + DWORD size = sizeof(DWORD); + DWORD data = 0; + LONG rc; + + *present = 0; + if (RegOpenKeyExW(HKEY_CURRENT_USER, g_key, 0, KEY_QUERY_VALUE, &k) != ERROR_SUCCESS) + return 1; + rc = RegQueryValueExW(k, name, NULL, &type, (BYTE *)&data, &size); + RegCloseKey(k); + if (rc == ERROR_FILE_NOT_FOUND) + return 1; + if (rc != ERROR_SUCCESS || type != REG_DWORD || size != sizeof(DWORD)) + return 0; + *value = data; + *present = 1; + return 1; +} + +static int reg_get_type(const wchar_t *name, DWORD *type) +{ + HKEY k; + wchar_t data[64]; + DWORD size = sizeof(data); + LONG rc; + + if (RegOpenKeyExW(HKEY_CURRENT_USER, g_key, 0, KEY_QUERY_VALUE, &k) != ERROR_SUCCESS) + return 0; + rc = RegQueryValueExW(k, name, NULL, type, (BYTE *)data, &size); + RegCloseKey(k); + return rc == ERROR_SUCCESS ? 1 : 0; +} + +static DWORD run_hook(const wchar_t *verb) +{ + wchar_t cmd[MAX_PATH * 2]; + STARTUPINFOW si; + PROCESS_INFORMATION pi; + DWORD code = 0xFFFFFFFFu; + + if (FAILED(StringCchPrintfW(cmd, SD_COUNTOF(cmd), L"\"%s\\sd-test-host-fake.exe\" --installer-hook webdav-tune %s", + g_dir, verb))) + return code; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + memset(&pi, 0, sizeof(pi)); + if (!CreateProcessW(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) + return code; + WaitForSingleObject(pi.hProcess, 30000); + GetExitCodeProcess(pi.hProcess, &code); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return code; +} + +static int state_path(wchar_t *out, size_t cch) +{ + return SUCCEEDED(StringCchPrintfW(out, cch, L"%s\\LibreStatic\\ShellDrive\\service\\state\\webdav-tune.ini", g_root)) + ? 1 + : 0; +} + +/* Writes a webdav-tune.ini under the test ProgramData root, creating the folders the hook uses. */ +static int write_state(const char *text) +{ + wchar_t dir[MAX_PATH]; + wchar_t file[MAX_PATH]; + HANDLE f; + DWORD put = 0; + + if (FAILED(StringCchPrintfW(dir, SD_COUNTOF(dir), L"%s\\LibreStatic", g_root)) || + !CreateDirectoryW(dir, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) + return 0; + if (FAILED(StringCchPrintfW(dir, SD_COUNTOF(dir), L"%s\\LibreStatic\\ShellDrive", g_root)) || + !CreateDirectoryW(dir, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) + return 0; + if (FAILED(StringCchPrintfW(dir, SD_COUNTOF(dir), L"%s\\LibreStatic\\ShellDrive\\service", g_root)) || + !CreateDirectoryW(dir, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) + return 0; + if (FAILED(StringCchPrintfW(dir, SD_COUNTOF(dir), L"%s\\LibreStatic\\ShellDrive\\service\\state", g_root)) || + !CreateDirectoryW(dir, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) + return 0; + if (!state_path(file, SD_COUNTOF(file))) + return 0; + f = CreateFileW(file, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (f == INVALID_HANDLE_VALUE) + return 0; + if (!WriteFile(f, text, (DWORD)strlen(text), &put, NULL) || put != (DWORD)strlen(text)) { + CloseHandle(f); + return 0; + } + CloseHandle(f); + return 1; +} + +/* Both limits are set, the originals are recorded, BasicAuthLevel is never touched, and --revert restores them. */ +SD_TEST(hooks_webdav_tune_apply_and_revert_restore_originals) +{ + wchar_t state[MAX_PATH]; + DWORD v = 0; + int present = 0; + + SD_REQUIRE(prepare()); + SD_REQUIRE(reg_set_dword(L"FileSizeLimitInBytes", 50000000)); + SD_REQUIRE(reg_set_dword(L"FileAttributesLimitInBytes", 1000000)); + SD_REQUIRE(reg_set_dword(L"BasicAuthLevel", 2)); + + SD_CHECK(run_hook(L"--apply") == 0); + SD_REQUIRE(state_path(state, SD_COUNTOF(state))); + SD_CHECK(GetFileAttributesW(state) != INVALID_FILE_ATTRIBUTES); + SD_REQUIRE(reg_get_dword(L"FileSizeLimitInBytes", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 0xFFFFFFFFu); + SD_REQUIRE(reg_get_dword(L"FileAttributesLimitInBytes", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 100000000u); + SD_REQUIRE(reg_get_dword(L"BasicAuthLevel", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 2); + + SD_CHECK(run_hook(L"--revert") == 0); + SD_REQUIRE(reg_get_dword(L"FileSizeLimitInBytes", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 50000000u); + SD_REQUIRE(reg_get_dword(L"FileAttributesLimitInBytes", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 1000000u); + SD_REQUIRE(reg_get_dword(L"BasicAuthLevel", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 2); + SD_CHECK(GetFileAttributesW(state) == INVALID_FILE_ATTRIBUTES); + + cleanup(); +} + +/* Values that did not exist are recorded as absent and deleted again; a repeat apply keeps the first originals. */ +SD_TEST(hooks_webdav_tune_records_absence_and_repeat_apply_keeps_originals) +{ + DWORD v = 0; + int present = 0; + + SD_REQUIRE(prepare()); + SD_CHECK(run_hook(L"--apply") == 0); + SD_REQUIRE(reg_get_dword(L"FileSizeLimitInBytes", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 0xFFFFFFFFu); + + /* Somebody changes the tuned values before the repair run: they are not the originals. */ + SD_REQUIRE(reg_set_dword(L"FileSizeLimitInBytes", 7)); + SD_REQUIRE(reg_set_dword(L"FileAttributesLimitInBytes", 8)); + SD_CHECK(run_hook(L"--apply") == 0); + + SD_CHECK(run_hook(L"--revert") == 0); + SD_REQUIRE(reg_get_dword(L"FileSizeLimitInBytes", &v, &present)); + SD_CHECK(!present); + SD_REQUIRE(reg_get_dword(L"FileAttributesLimitInBytes", &v, &present)); + SD_CHECK(!present); + /* BasicAuthLevel was never created by the hook. */ + SD_REQUIRE(reg_get_dword(L"BasicAuthLevel", &v, &present)); + SD_CHECK(!present); + + cleanup(); +} + +/* A revert with no record is a no-op: the hook never applied, or an older install. */ +SD_TEST(hooks_webdav_tune_revert_without_state_is_a_noop) +{ + DWORD v = 0; + int present = 0; + + SD_REQUIRE(prepare()); + SD_REQUIRE(reg_set_dword(L"FileSizeLimitInBytes", 12345)); + SD_CHECK(run_hook(L"--revert") == 0); + SD_REQUIRE(reg_get_dword(L"FileSizeLimitInBytes", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 12345u); + cleanup(); +} + +/* A damaged record is refused by both verbs; the values are left as they are. */ +SD_TEST(hooks_webdav_tune_refuses_a_damaged_state) +{ + DWORD v = 0; + int present = 0; + + SD_REQUIRE(prepare()); + SD_REQUIRE(reg_set_dword(L"FileSizeLimitInBytes", 111)); + SD_REQUIRE(reg_set_dword(L"FileAttributesLimitInBytes", 222)); + SD_REQUIRE(write_state("[webdav-tune]\r\nfile_size_limit_in_bytes = notanumber\r\n" + "file_attributes_limit_in_bytes = absent\r\n")); + + SD_CHECK(run_hook(L"--apply") != 0); + SD_REQUIRE(reg_get_dword(L"FileSizeLimitInBytes", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 111u); + SD_REQUIRE(reg_get_dword(L"FileAttributesLimitInBytes", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 222u); + + SD_CHECK(run_hook(L"--revert") != 0); + SD_REQUIRE(reg_get_dword(L"FileSizeLimitInBytes", &v, &present)); + SD_CHECK(present); + SD_CHECK(v == 111u); + + cleanup(); +} + +/* A value that is not a DWORD is not ours: the hook fails closed instead of replacing or deleting it. */ +SD_TEST(hooks_webdav_tune_refuses_a_value_of_another_type) +{ + wchar_t state[MAX_PATH]; + DWORD type = 0; + + SD_REQUIRE(prepare()); + SD_REQUIRE(reg_set_sz(L"FileSizeLimitInBytes", L"nope")); + SD_REQUIRE(reg_set_dword(L"FileAttributesLimitInBytes", 222)); + + SD_CHECK(run_hook(L"--apply") != 0); + SD_REQUIRE(reg_get_type(L"FileSizeLimitInBytes", &type)); + SD_CHECK(type == REG_SZ); + /* Nothing was recorded either: the value check comes before the state file. */ + SD_REQUIRE(state_path(state, SD_COUNTOF(state))); + SD_CHECK(GetFileAttributesW(state) == INVALID_FILE_ATTRIBUTES); + cleanup(); +} From 636fa8794a309d6254c501750a4ab793d7447c05 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 30 Oct 2026 16:48:24 -0300 Subject: [PATCH 2/3] m4: build the ARM64 and ARM32 MSIs (D31) An arm64 or arm32 tree with SD_BUILD_MSI takes sd-msigen, sd-msiembed and sd-test-msi from its nested x64 host-tools build, so it builds its own MSI and runs the msi.* checks on the x64 host. - packaging/msi: arm64 and arm32 rows of the flavour table (04 section 8.2). ARM64 also carries the x86-sse2 shell extension in the 32-bit view. - ShellDrive.wxs: SD_UI and SD_DOKANY defines. The ARM32 package has Core and Autostart only and no Dokany option. Its launch condition refuses Windows RT and builds 26100 and later. - sd-msigen registry --ui no drops the shell extension and App Paths rows. - New installer string msi.os.arm32 in all six catalogs. - sd-test-msi: ui and platform arguments; ARM32 layout and launch condition checks; the admin image checks the PE machine of shelldrive.exe. - ARM ctest presets select ^msi[.]; ci-local builds the ARM MSIs when WiX is installed and runs their checks in Release. Gate: tools/ci-local.ps1 green on x64-modern, x64-xp, x86-ia32, x86-sse2, x64-analysis, arm64 and arm32 (msi.arm64.* and msi.arm32.* pass). --- CMakeLists.txt | 6 ++ CMakePresets.json | 8 +- cmake/SdHostTools.cmake | 15 ++- cmake/SdOptions.cmake | 1 + docs/design/04-build-install-update.md | 14 ++- docs/dev/m4-notes.md | 54 +++++++++- l10n/installer/de.po | 5 + l10n/installer/en.po | 5 + l10n/installer/es.po | 5 + l10n/installer/fr.po | 5 + l10n/installer/it.po | 5 + l10n/installer/pt-BR.po | 5 + packaging/msi/CMakeLists.txt | 71 ++++++++++---- packaging/msi/ShellDrive.wxs | 32 +++++- packaging/msi/build-msi.cmake | 18 +++- tests/msi/CMakeLists.txt | 56 +++++++---- tests/msi/sd-test-msi.c | 131 ++++++++++++++++++++----- tools/ci-local.ps1 | 17 ++-- tools/sd-msigen/sd-msigen.c | 15 ++- 19 files changed, 384 insertions(+), 84 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2379167..3924a8d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,12 @@ if(SD_HOST_TOOLS_ONLY) add_subdirectory(tools/sd-importcheck) add_subdirectory(tools/sd-admxgen) add_subdirectory(tools/sd-payload) + if(SD_HOST_TOOLS_MSI) + # The MSI tools of an ARM tree with SD_BUILD_MSI (packaging/msi, tests/msi). + add_subdirectory(tools/sd-msigen) + add_subdirectory(tools/sd-msiembed) + add_subdirectory(tests/msi) + endif() return() endif() diff --git a/CMakePresets.json b/CMakePresets.json index 5a0df54..ad57b2c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -291,12 +291,12 @@ }, { "name": "arm64", - "description": "Build-only flavour: ARM64 test binaries cannot run on x64 hosts (04 section 11.4). This preset selects no tests, so ctest exits 0 and reports nothing to run.", + "description": "ARM64 test binaries cannot run on x64 hosts (04 section 11.4). This preset selects only the msi.* checks, which run the x64 sd-test-msi of the nested host-tools build on the package (-DSD_BUILD_MSI=ON); without the MSI it selects nothing and ctest exits 0.", "configurePreset": "arm64", "configuration": "Release", "filter": { "include": { - "name": "^$" + "name": "^msi[.]" } }, "execution": { @@ -305,12 +305,12 @@ }, { "name": "arm32", - "description": "Build-only flavour: ARM32 test binaries cannot run on x64 hosts (04 section 11.4). This preset selects no tests, so ctest exits 0 and reports nothing to run.", + "description": "ARM32 test binaries cannot run on x64 hosts (04 section 11.4). This preset selects only the msi.* checks, which run the x64 sd-test-msi of the nested host-tools build on the package (-DSD_BUILD_MSI=ON); without the MSI it selects nothing and ctest exits 0.", "configurePreset": "arm32", "configuration": "Release", "filter": { "include": { - "name": "^$" + "name": "^msi[.]" } }, "execution": { diff --git a/cmake/SdHostTools.cmake b/cmake/SdHostTools.cmake index 0ecd5e1..4c57494 100644 --- a/cmake/SdHostTools.cmake +++ b/cmake/SdHostTools.cmake @@ -9,11 +9,22 @@ # sd_host_tools_setup() called once by the root CMakeLists.txt (ARM64 and ARM32 trees only). # sd_add_tool_dependency( ) runs in a build step: build it first. # sd_tool_depends( ) DEPENDS list for a custom command that runs . +# +# With SD_BUILD_MSI the nested build also has the MSI tools (SD_HOST_TOOLS_MSI): sd-msigen and sd-msiembed, which +# packaging/msi runs, and sd-test-msi, which the msi.* CTests run on the package (tests/msi). So an ARM tree builds its +# MSI and checks it on this host, the way an x86 or x64 tree does (04 §8, D31). set(SD_HOST_TOOLS_NAMES sdl10n sd-importcheck sd-admxgen sd-payload) +set(SD_HOST_TOOLS_MSI_NAMES sd-msigen sd-msiembed sd-test-msi) function(sd_host_tools_setup) set(_bin "${CMAKE_BINARY_DIR}/host-tools") + set(_names ${SD_HOST_TOOLS_NAMES}) + set(_msi OFF) + if(SD_BUILD_MSI) + set(_msi ON) + list(APPEND _names ${SD_HOST_TOOLS_MSI_NAMES}) + endif() include(ExternalProject) ExternalProject_Add(sd_host_tools SOURCE_DIR "${CMAKE_SOURCE_DIR}" @@ -21,11 +32,11 @@ function(sd_host_tools_setup) PREFIX "${CMAKE_BINARY_DIR}/host-tools-prefix" CMAKE_GENERATOR "${CMAKE_GENERATOR}" CMAKE_GENERATOR_PLATFORM x64 - CMAKE_ARGS -DSD_HOST_TOOLS_ONLY=ON -DSD_FLAVOUR=x64-modern + CMAKE_ARGS -DSD_HOST_TOOLS_ONLY=ON -DSD_FLAVOUR=x64-modern -DSD_HOST_TOOLS_MSI=${_msi} INSTALL_COMMAND "" BUILD_ALWAYS ON) set_target_properties(sd_host_tools PROPERTIES FOLDER "tools") - foreach(_t IN LISTS SD_HOST_TOOLS_NAMES) + foreach(_t IN LISTS _names) add_executable(${_t} IMPORTED GLOBAL) set_target_properties(${_t} PROPERTIES IMPORTED_LOCATION_DEBUG "${_bin}/bin/Debug/${_t}.exe" diff --git a/cmake/SdOptions.cmake b/cmake/SdOptions.cmake index 63069a3..b3652bd 100644 --- a/cmake/SdOptions.cmake +++ b/cmake/SdOptions.cmake @@ -7,6 +7,7 @@ set(SD_FLAVOUR "" CACHE STRING "Build flavour (x86-ia32, x86-sse2, x64-modern, x set_property(CACHE SD_FLAVOUR PROPERTY STRINGS x86-ia32 x86-sse2 x64-modern x64-xp x64-analysis arm64 arm32) # Internal: the nested x64 configure that builds the build-time tools for an ARM64 or ARM32 tree (cmake/SdHostTools.cmake). option(SD_HOST_TOOLS_ONLY "Internal: configure only the build-time tools for an ARM64 or ARM32 tree" OFF) +option(SD_HOST_TOOLS_MSI "Internal: the host tools also include the MSI tools (the ARM tree has SD_BUILD_MSI)" OFF) # Internal: the nested x86 configure that builds only the 32-bit shell extension for an x64 MSI (cmake/SdShellExtX86.cmake). option(SD_SHELLEXT_ONLY "Internal: configure only the x86 shell extension for the x64 MSI" OFF) set(SD_X86_ARCH "" CACHE STRING "x86 flavours: code generation floor (IA32 or SSE2, decisions.md D4)") diff --git a/docs/design/04-build-install-update.md b/docs/design/04-build-install-update.md index d2280f3..e6d4c49 100644 --- a/docs/design/04-build-install-update.md +++ b/docs/design/04-build-install-update.md @@ -400,8 +400,9 @@ or pre-Windows 10 x64 machines; the MSI launch conditions (§8.2) refuse the wro - Implementation status (`wip/r7-arm32`, M6 notes "ARM32 flavour"): the `arm32` preset builds Release and Debug with no warnings; the UI, the shell extension and their tests are not built; the setup stub is native ARM32 and installs the `PAYLOAD_ARM32` CAB (CLI, host, uninstall.exe). CI and `tools/ci-local.ps1` build it without tests. - Not implemented yet: the Windows 11 24H2 and RT/IoT refusals below, the ARM32 MSI (D31), and the ARM32 - constant-time gate and test run (open items in the M6 notes). + The ARM32 MSI (D31) is built by the `arm32` preset with `-DSD_BUILD_MSI=ON` (§8, open item 9): CLI and host only, + with the 24H2 and RT refusal as its launch condition. Not implemented yet: the same refusals in the setup EXE, and + the ARM32 constant-time gate and test run (open items in the M6 notes). - Targets, with their limits: - **Windows 10 on ARM64 and Windows 11 up to 23H2 (ARM64 hardware)** run ARM32 desktop apps and include Windows Installer 5.0 and WebClient. This is the only realistic target and the one the lab tests. @@ -1255,8 +1256,13 @@ to the lab (11.4). repair or maintenance keeps them. Still open: the lab check of the Installer key and of the ownership rules (install, Change, repair, uninstall, and a pre-existing value), and the Dokany driver, which the package does not install or remove. 8. Reproducibility (12.2): the summary `Created` and `LastSaved` times vary between builds; `SOURCE_DATE_EPOCH` is not applied yet. -9. ARM32 (D31): no `arm32` preset, so the package is not built. The ARM64 package needs the M6 host-tools path, because an ARM64 - tree cannot run the MSI tools. `packaging/msi/CMakeLists.txt` refuses `SD_BUILD_MSI` on ARM64 trees. +9. ARM64 and ARM32 (D31): built (M4). An `arm64` or `arm32` tree with `-DSD_BUILD_MSI=ON` takes `sd-msigen`, `sd-msiembed` + and `sd-test-msi` from its nested x64 host-tools build (§2.5, `cmake/SdHostTools.cmake`), so the package and its + `msi..*` checks run on the x64 build host. ARM64: `Arm64` template, the x64 parameters otherwise, plus the + x86-sse2 shell extension in the 32-bit view. ARM32: `Arm` template, Program Files folder of 32-bit packages, `Core` and + `Autostart` only, no Dokany option, launch condition `SD_OSMAJOR >= 10 AND SD_OSBUILD < 26100`. Open: the lab items of + §5.1 (template acceptance, install folder, the build cut-off), and the x64 shell extension that x64 apps under emulation + on ARM64 would load (§11 row 11). 10. Lab checks: the install-time behaviour of the hooks, of `ENABLELINKEDCONNECTIONS` (setup returns 3010, 8.4), of the `DOKANY` and `UPDATE*` settings, and of the uninstall removal (TST-27) is not verified on this machine. diff --git a/docs/dev/m4-notes.md b/docs/dev/m4-notes.md index cd674db..0adcd72 100644 --- a/docs/dev/m4-notes.md +++ b/docs/dev/m4-notes.md @@ -777,7 +777,7 @@ another milestone. Evidence paths are relative to the repository root. | Setup stub from the `x86-ia32` preset, CAB payloads | **Done** | `apps/shelldrive-setup/` (`shelldrive-setup-stub`, `shelldrive-setup`), `cmake/SdPayload.cmake` (FDI CAB via `cabinet.dll`, fixed member times), `tests/setup` (`sd-test-engine`, 54 tests), import and ISA audits | | Per-user, per-machine and portable | **Done in code; lab pending** | `setup_install.c`, `setup_elevate.c` (two stages, pipe relay, per-machine uninstall), `tests/setup/test_elevate.c`. Lab: UAC/Run As, msiexec, 3010 (04 §11.4) | | Six flavours and stub selection by CPU and OS (D4, D28) | **Partial** | Presets for all six (`CMakePresets.json`); `sd_setup_select_flavour` covers x86 SSE2, x64 by OS version and the ARM paths. Stub and payload targets exist only in the `x86-ia32`, `arm64` and `arm32` presets: **no preset builds an x64 stub or payload**, and the universal EXE that carries `PAYLOAD_X86_SSE2`, `PAYLOAD_X64_XP` and `PAYLOAD_X64_MODERN` is release-pipeline work (`tools/sd-payload` attaches one flavour per run). The `x86-ia32` EXE built here therefore selects `x86-sse2` on an SSE2 host and reports `payload_missing` until the pipeline assembles the universal EXE. ARM stubs are cross-built and cannot run here (04 §11.4) | -| WiX MSIs per flavour, incl. ARM32 (D31 best effort per D5) | **Partial / Missing (ARM)** | x86 pair and x64 pair build; `msi..tables`, `.admin` (admin image only) and `.wixui` pass. `packaging/msi/CMakeLists.txt` refuses `SD_ARM64` and has no ARM32 row. `decisions.md` D31 requires the ARM32 MSI (the review table's "(a) no" is superseded). Both need a cross-tree `msi` target with host-built generator tools | +| WiX MSIs per flavour, incl. ARM32 (D31 best effort per D5) | **Done (build and `msiexec /a` checks); install lab pending** | All six flavours build. x86 pair and x64 pair as before; ARM64 and ARM32 added later in this change (section "ARM64 and ARM32 MSIs" below). `msi..tables`, `.admin` (admin image only) and `.wixui` pass on `x86-ia32`, `x64-modern`, `arm64` and `arm32` | | Language transforms (C43) | **Partial** | `sd-msigen` LCID table (es under 3082/11274/1034/2058/21514/…, pt-BR under 1046/2070), `sd-msiembed`, checked by `msi..tables`. **The setup EXE never passes `TRANSFORMS` explicitly** (the string appears only in `sd-msiembed`), so a `/lang=` choice that differs from the OS language gets the OS transform on a per-machine install | | Dokany chaining (C31, D32) | **Partial** | `setup_dokany.c`: decision, PE import reader, command lines and run gate, 5 tests. Nothing calls `sd_setup_dokany_run`; no `DOKANY_` payload member, no `installer/dokany/dokany.lock`, no post-install `--selftest` probe. C31 fixes the form: official per-arch MSIs | | `webdav-tune` and the other installer hooks | **Partial → webdav-tune closed here** | `signal-shutdown`, `user-cleanup` and `programdata-secure` implemented; `modern-menu` is a deliberate no-op until the sparse package exists (D27); `webdav-tune` was a silent no-op and is implemented in this change (below) | @@ -847,3 +847,55 @@ started it. When sshd on port 2222 was started from another checkout, `start.ps1 worktree has no `state/env`, so `interop.ssh`, `interop.zlib` and `interop.sftp_ssh` exit 77 and `ci-local` fails on the skip. Run the gate with `SD_SSH_WSL_FIXTURE_DIR` set to the `tests/fixtures/wsl-sshd` folder of the checkout that owns the running fixture. + +## ARM64 and ARM32 MSIs (D31, built in M4) + +Decision (maintainer, 2026-10-30): the ARM MSIs are built in M4, not deferred to M6 with the ARM64 test job. + +**How an ARM tree builds its MSI.** The MSI tools must run on the x64 build host, and an ARM tree's own executables +cannot. The ARM presets already build their build-time tools (`sdl10n`, `sd-importcheck`, ...) in a nested x64 +configure (`cmake/SdHostTools.cmake`, 04 §2.5). With `-DSD_BUILD_MSI=ON` that nested build also gets +`SD_HOST_TOOLS_MSI=ON` and builds `sd-msigen`, `sd-msiembed` and `sd-test-msi`; the ARM tree uses them as imported +executables, and its `msi` target depends on `sd_host_tools`. WiX (candle, light, torch) is an x86 .NET program, so +nothing else changes. The package is built from the ARM tree's own binaries: no second build directory is involved. + +| | `arm64` | `arm32` | +|---|---|---| +| candle `-arch`, Template | `arm64`, `Arm64;1033,...` | `arm`, `Arm;1033,...` | +| Program Files folder, `Win64` | `ProgramFiles64Folder`, yes | `ProgramFilesFolder`, no | +| `InstallerVersion` | 500 | 500 | +| UpgradeCode (04 §8.2) | `{1438051B-3E86-459D-8199-E8D786F5A32B}` | `{23F62FB5-0CD9-4087-B04C-D2A1B4A0A8F2}` | +| Contents | as x64: CLI, host, UI, shell extension, Start menu, plus the x86-sse2 shell extension in the 32-bit view (x86 apps under WOW64) | `Core` (CLI and host) and `Autostart` only (04 §5.1) | +| Options page | Dokany and linked connections | linked connections only; `SD_DOKANY_STATE` = 0 (no ARM32 Dokany, 04 §7.1) | +| Launch condition | none beyond the downgrade check | `SD_OSMAJOR >= 10 AND SD_OSBUILD < 26100` | + +ARM32 details: + +- `ShellDrive.wxs` has two new defines, `SD_UI` and `SD_DOKANY` (both 0 for ARM32). They leave out `C_Core_Ui`, + `C_ShellExt`, `C_StartMenu`, the `ShellExt` and `StartMenu` features and the Dokany checkbox. +- `sd-msigen registry --ui no` drops the shellext rows and the `App Paths\shelldrive-ui.exe` row: their components share + one fragment with the core rows, so leaving them in would link components that no feature references. +- The launch condition: Windows RT 8.1 has no `CurrentMajorVersionNumber`, so `SD_OSMAJOR >= 10` refuses it; build 26100 + (Windows 11 24H2) and later run no ARM32 app. `SD_OSBUILD` is `CurrentBuildNumber` (REG_SZ). The message is the new + installer string `msi.os.arm32` (WiX id `OsArm32`), in all six catalogs. **[lab-verify]** the 32-bit registry view + of an ARM32 package on ARM64 has `CurrentBuildNumber`, and Windows Installer compares it as a number. + +**Checks** (`sd-test-msi`, from the nested x64 build). The ARM ctest presets now select `^msi[.]`. Without the MSI +nothing matches and ctest still exits 0. `tools/ci-local.ps1` configures `arm64` and `arm32` with `SD_BUILD_MSI` when +WiX 3.14 is installed and runs those checks in Release. The Debug configuration and the GitHub `ci.yml` stay build-only. + +- `tables` takes a new `` argument. With 0 it checks: no UI, shell extension or Start menu component, feature, file + or row; no `App Paths` row; no Dokany control; `SD_DOKANY_STATE` = 0; the ARM32 launch condition and its + `SD_OSBUILD` search. +- `admin` now takes ` `. It checks the files of the flavour (two for ARM32, and that the + UI and the DLL are absent), and that `shelldrive.exe` in the admin image is a PE of the package's machine: i386, + AMD64, ARM64 or ARMNT. `msiexec /a` of the ARM packages works on this x64 host. + +Open (lab or later): + +- The 04 §5.1 [lab-verify] items: Windows Installer on ARM64 accepts the `Arm` template; the folder ARM32 packages + resolve to. +- The ARM64 package carries no x64 shell extension for x64 apps under emulation (04 §11.4 row "11 ARM64"). +- The setup EXE's own ARM32 24H2/RT refusal (04 §5.1) is not implemented. +- GitHub CI does not build any MSI (`ci.yml` passes no `SD_BUILD_MSI`); the release pipeline's `package` job is still + a stub. diff --git a/l10n/installer/de.po b/l10n/installer/de.po index a3e2b93..e3a3c98 100644 --- a/l10n/installer/de.po +++ b/l10n/installer/de.po @@ -373,6 +373,11 @@ msgctxt "msi.os.required" msgid "This package of ShellDrive needs Windows 10 or later. Use the package for your version of Windows." msgstr "Dieses ShellDrive-Paket benötigt Windows 10 oder neuer. Nimm das Paket für deine Windows-Version." +#. WiX string OsArm32 of the ARM32 MSI (launch condition, 04 §5.1): Windows 10 on ARM or Windows 11 up to 23H2. +msgctxt "msi.os.arm32" +msgid "This package of ShellDrive needs Windows 10 on ARM or Windows 11 version 23H2 or earlier: later versions of Windows no longer run 32-bit ARM apps. Use the ARM64 package." +msgstr "Dieses ShellDrive-Paket benötigt Windows 10 auf ARM oder Windows 11 Version 23H2 oder älter: Neuere Windows-Versionen führen keine 32-Bit-ARM-Apps mehr aus. Nimm das ARM64-Paket." + # WiX string OptionsTitle of the options page of the Change dialog set (04 §6.7). msgctxt "msi.options.title" msgid "Options of this installation" diff --git a/l10n/installer/en.po b/l10n/installer/en.po index cfe9cb5..e7b512f 100644 --- a/l10n/installer/en.po +++ b/l10n/installer/en.po @@ -412,6 +412,11 @@ msgctxt "msi.os.required" msgid "This package of ShellDrive needs Windows 10 or later. Use the package for your version of Windows." msgstr "" +#. WiX string OsArm32 of the ARM32 MSI (launch condition, 04 §5.1): Windows 10 on ARM or Windows 11 up to 23H2. +msgctxt "msi.os.arm32" +msgid "This package of ShellDrive needs Windows 10 on ARM or Windows 11 version 23H2 or earlier: later versions of Windows no longer run 32-bit ARM apps. Use the ARM64 package." +msgstr "" + # WiX string OptionsTitle of the options page of the Change dialog set (04 §6.7). msgctxt "msi.options.title" msgid "Options of this installation" diff --git a/l10n/installer/es.po b/l10n/installer/es.po index 22b165b..40d80cd 100644 --- a/l10n/installer/es.po +++ b/l10n/installer/es.po @@ -374,6 +374,11 @@ msgctxt "msi.os.required" msgid "This package of ShellDrive needs Windows 10 or later. Use the package for your version of Windows." msgstr "Este paquete de ShellDrive necesita Windows 10 o una versión posterior. Usá el paquete que corresponde a tu versión de Windows." +#. WiX string OsArm32 of the ARM32 MSI (launch condition, 04 §5.1): Windows 10 on ARM or Windows 11 up to 23H2. +msgctxt "msi.os.arm32" +msgid "This package of ShellDrive needs Windows 10 on ARM or Windows 11 version 23H2 or earlier: later versions of Windows no longer run 32-bit ARM apps. Use the ARM64 package." +msgstr "Este paquete de ShellDrive necesita Windows 10 en ARM o Windows 11 versión 23H2 o anterior: las versiones posteriores de Windows ya no ejecutan apps ARM de 32 bits. Usá el paquete ARM64." + # WiX string OptionsTitle of the options page of the Change dialog set (04 §6.7). msgctxt "msi.options.title" msgid "Options of this installation" diff --git a/l10n/installer/fr.po b/l10n/installer/fr.po index 2f1148a..9ca1628 100644 --- a/l10n/installer/fr.po +++ b/l10n/installer/fr.po @@ -374,6 +374,11 @@ msgctxt "msi.os.required" msgid "This package of ShellDrive needs Windows 10 or later. Use the package for your version of Windows." msgstr "Ce paquet de ShellDrive nécessite Windows 10 ou une version ultérieure. Veuillez utiliser le paquet correspondant à votre version de Windows." +#. WiX string OsArm32 of the ARM32 MSI (launch condition, 04 §5.1): Windows 10 on ARM or Windows 11 up to 23H2. +msgctxt "msi.os.arm32" +msgid "This package of ShellDrive needs Windows 10 on ARM or Windows 11 version 23H2 or earlier: later versions of Windows no longer run 32-bit ARM apps. Use the ARM64 package." +msgstr "Ce paquet de ShellDrive nécessite Windows 10 sur ARM ou Windows 11 version 23H2 ou antérieure : les versions ultérieures de Windows n'exécutent plus les applications ARM 32 bits. Veuillez utiliser le paquet ARM64." + # WiX string OptionsTitle of the options page of the Change dialog set (04 §6.7). msgctxt "msi.options.title" msgid "Options of this installation" diff --git a/l10n/installer/it.po b/l10n/installer/it.po index ce928d2..aab0770 100644 --- a/l10n/installer/it.po +++ b/l10n/installer/it.po @@ -374,6 +374,11 @@ msgctxt "msi.os.required" msgid "This package of ShellDrive needs Windows 10 or later. Use the package for your version of Windows." msgstr "Questo pacchetto di ShellDrive richiede Windows 10 o una versione successiva. Usa il pacchetto corrispondente alla tua versione di Windows." +#. WiX string OsArm32 of the ARM32 MSI (launch condition, 04 §5.1): Windows 10 on ARM or Windows 11 up to 23H2. +msgctxt "msi.os.arm32" +msgid "This package of ShellDrive needs Windows 10 on ARM or Windows 11 version 23H2 or earlier: later versions of Windows no longer run 32-bit ARM apps. Use the ARM64 package." +msgstr "Questo pacchetto di ShellDrive richiede Windows 10 su ARM o Windows 11 versione 23H2 o precedente: le versioni successive di Windows non eseguono più app ARM a 32 bit. Usa il pacchetto ARM64." + # WiX string OptionsTitle of the options page of the Change dialog set (04 §6.7). msgctxt "msi.options.title" msgid "Options of this installation" diff --git a/l10n/installer/pt-BR.po b/l10n/installer/pt-BR.po index a8d41ef..3db278a 100644 --- a/l10n/installer/pt-BR.po +++ b/l10n/installer/pt-BR.po @@ -374,6 +374,11 @@ msgctxt "msi.os.required" msgid "This package of ShellDrive needs Windows 10 or later. Use the package for your version of Windows." msgstr "Este pacote do ShellDrive precisa do Windows 10 ou de uma versão posterior. Use o pacote correspondente à sua versão do Windows." +#. WiX string OsArm32 of the ARM32 MSI (launch condition, 04 §5.1): Windows 10 on ARM or Windows 11 up to 23H2. +msgctxt "msi.os.arm32" +msgid "This package of ShellDrive needs Windows 10 on ARM or Windows 11 version 23H2 or earlier: later versions of Windows no longer run 32-bit ARM apps. Use the ARM64 package." +msgstr "Este pacote do ShellDrive precisa do Windows 10 em ARM ou do Windows 11 versão 23H2 ou anterior: as versões posteriores do Windows não executam mais aplicativos ARM de 32 bits. Use o pacote ARM64." + # WiX string OptionsTitle of the options page of the Change dialog set (04 §6.7). msgctxt "msi.options.title" msgid "Options of this installation" diff --git a/packaging/msi/CMakeLists.txt b/packaging/msi/CMakeLists.txt index a05b74a..9d4aa79 100644 --- a/packaging/msi/CMakeLists.txt +++ b/packaging/msi/CMakeLists.txt @@ -4,15 +4,17 @@ # # builds the MSI of this preset's flavour from the binaries of the preset (packaging/msi/build-msi.cmake): # sd-msigen, candle, light (ICE validation on the base package), torch and sd-msiembed. The MSI tools run on the -# build host, so an ARM64 tree cannot build it (the ARM64 MSI is M6, 04 §1.1). +# build host: an ARM64 or ARM32 tree takes sd-msigen and sd-msiembed from its nested x64 build (cmake/SdHostTools.cmake, +# SD_HOST_TOOLS_MSI), the way it takes sdl10n; WiX is an x86 .NET program that the host runs either way. -if(SD_ARM64) - message(FATAL_ERROR "SD_BUILD_MSI needs an x86 or x64 preset: the MSI tools run on the build host (04 §8).") +if(SD_CROSS_HOST_TOOLS) + set(_msi_tool_deps "") # the msi target depends on sd_host_tools instead +else() + add_subdirectory(${CMAKE_SOURCE_DIR}/tools/sd-msigen ${CMAKE_BINARY_DIR}/tools/sd-msigen) + add_subdirectory(${CMAKE_SOURCE_DIR}/tools/sd-msiembed ${CMAKE_BINARY_DIR}/tools/sd-msiembed) + set(_msi_tool_deps sd-msigen sd-msiembed) endif() -add_subdirectory(${CMAKE_SOURCE_DIR}/tools/sd-msigen ${CMAKE_BINARY_DIR}/tools/sd-msigen) -add_subdirectory(${CMAKE_SOURCE_DIR}/tools/sd-msiembed ${CMAKE_BINARY_DIR}/tools/sd-msiembed) - # WiX Toolset 3.14 (04 §8.1): the WIX environment variable of the WiX installer, else its default folder. if(DEFINED ENV{WIX} AND NOT "$ENV{WIX}" STREQUAL "") set(_sd_wix_default "$ENV{WIX}") @@ -26,8 +28,9 @@ if(NOT EXISTS "${SD_WIX_DIR}/bin/candle.exe") endif() # Per-flavour package parameters (04 §8.2). UpgradeCodes are fixed forever; the x86 pair and the x64 pair share one. -# The arm32 row is authored for the day an arm32 preset exists (D31); there is none, so it is not built. # family arch Program Files folder win64 installer launch UpgradeCode +# ARM32 (D31, 04 §5.1) has the CLI and the host only: no UI, no shell extension, no Start menu, no Dokany (_ui 0). +set(_ui 1) if(SD_FLAVOUR STREQUAL "x86-ia32" OR SD_FLAVOUR STREQUAL "x86-sse2") set(_family x86) set(_arch x86) @@ -52,19 +55,37 @@ elseif(SD_FLAVOUR STREQUAL "x64-modern") set(_installer 500) set(_launch win10) set(_upgrade "{37D2227E-A917-46A7-8F8E-0F9A8146CA5F}") +elseif(SD_FLAVOUR STREQUAL "arm64") + set(_family arm64) + set(_arch arm64) + set(_pf ProgramFiles64Folder) + set(_win64 yes) + set(_installer 500) + set(_launch none) + set(_upgrade "{1438051B-3E86-459D-8199-E8D786F5A32B}") +elseif(SD_FLAVOUR STREQUAL "arm32") + set(_family arm32) + set(_arch arm) + set(_pf ProgramFilesFolder) + set(_win64 no) + set(_installer 500) + set(_launch arm32) + set(_upgrade "{23F62FB5-0CD9-4087-B04C-D2A1B4A0A8F2}") + set(_ui 0) else() message(FATAL_ERROR "SD_BUILD_MSI: no MSI for the flavour ${SD_FLAVOUR} (04 §8.2)") endif() -# The 32-bit shell extension (04 §6.2, §8.3; 03 §8.5). An x64 package also carries the x86-sse2 copy of the DLL in -# INSTALLDIR† and registers it in the 32-bit registry view (32-bit apps' file dialogs load it). The copy comes from -# a nested x86 configure (cmake/SdShellExtX86.cmake). The x86 packages have no 32-bit view to add: their DLL is the -# 32-bit one already. +# The 32-bit shell extension (04 §6.2, §8.3; 03 §8.5). An x64 or ARM64 package also carries the x86-sse2 copy of the +# DLL in Program Files (x86) and registers it in the 32-bit registry view (32-bit apps' file dialogs load it; on ARM64 +# the x86 apps run under WOW64 with the same view). The copy comes from a nested x86 configure +# (cmake/SdShellExtX86.cmake). The x86 packages have no 32-bit view to add: their DLL is the 32-bit one already. The +# ARM32 package has no shell extension. set(SD_MSI_SHELLEXT32 0) -set(SD_MSI_BIN32 "$") +set(SD_MSI_BIN32 "$") # any existing folder when there is no 32-bit copy set(_msi_x86_deps "") set(SD_MSI_TEST_X86_DLL "") -if(_arch STREQUAL "x64") +if(_arch STREQUAL "x64" OR _arch STREQUAL "arm64") include(SdShellExtX86) sd_add_x86_shellext(_sd_x86_shellext_target) set(SD_MSI_SHELLEXT32 1) @@ -76,6 +97,11 @@ endif() # The service (M5, D6) is not there yet: its ServiceInstall stays behind this define (ShellDrive.wxs). set(SD_MSI_WITH_SERVICE 0 CACHE STRING "1 once the service binary exists (M5): ServiceInstall/ServiceControl on") +set(_msi_ui_deps "") +if(_ui) + set(_msi_ui_deps shelldrive-ui shelldrive-shellext) +endif() + set(_msi_version "${SD_VERSION_MAJOR}.${SD_VERSION_MINOR}.${SD_VERSION_PATCH}") set(_msi_dir "${CMAKE_BINARY_DIR}/msi/$") set(_msi_out "${_msi_dir}/ShellDrive-${_msi_version}-${SD_FLAVOUR}.msi") @@ -99,6 +125,8 @@ add_custom_command( "-DSD_MSI_BIN=$" "-DSD_MSI_SHELLEXT32=${SD_MSI_SHELLEXT32}" "-DSD_MSI_BIN32=${SD_MSI_BIN32}" + "-DSD_MSI_UI=${_ui}" + "-DSD_MSI_DOKANY=${_ui}" "-DSD_MSI_ICON=${CMAKE_SOURCE_DIR}/res/icons/shelldrive.ico" "-DSD_MSI_SRC=${CMAKE_CURRENT_SOURCE_DIR}" "-DSD_MSI_GEN=${_msi_gen}" @@ -110,7 +138,7 @@ add_custom_command( "-DSD_MSIGEN=$" "-DSD_MSIEMBED=$" -P "${CMAKE_CURRENT_SOURCE_DIR}/build-msi.cmake" - DEPENDS sd-msigen sd-msiembed shelldrive shelldrive-host shelldrive-ui shelldrive-shellext ${_msi_x86_deps} + DEPENDS ${_msi_tool_deps} shelldrive shelldrive-host ${_msi_ui_deps} ${_msi_x86_deps} "${CMAKE_CURRENT_SOURCE_DIR}/build-msi.cmake" "${CMAKE_CURRENT_SOURCE_DIR}/ShellDrive.wxs" "${CMAKE_SOURCE_DIR}/packaging/registration.txt" "${CMAKE_SOURCE_DIR}/LICENSE" ${_msi_po} ${_msi_wixui} COMMENT "msi: ShellDrive ${SD_FLAVOUR} ${_msi_version}" @@ -118,17 +146,25 @@ add_custom_command( add_custom_target(msi DEPENDS "${_msi_out}") set_target_properties(msi PROPERTIES FOLDER "packaging") +if(SD_CROSS_HOST_TOOLS) + add_dependencies(msi sd_host_tools) +endif() if(SD_MSI_SHELLEXT32) add_dependencies(msi sd_shellext_x86) endif() -# Parameters of the CTest checks in tests/msi (the root scope sees them: packaging/msi is added first). +# Parameters of the CTest checks in tests/msi (the root scope sees them: packaging/msi is added first). The platform is +# the Template prefix of the summary information (04 §8.2). if(_arch STREQUAL "x86") set(_platform Intel) -else() +elseif(_arch STREQUAL "x64") set(_platform x64) +elseif(_arch STREQUAL "arm64") + set(_platform Arm64) +else() + set(_platform Arm) endif() -if(_launch STREQUAL "win10") +if(NOT _launch STREQUAL "none") set(_launch_count 2) else() set(_launch_count 1) @@ -141,3 +177,4 @@ set(SD_MSI_TEST_INSTALLER "${_installer}" PARENT_SCOPE) set(SD_MSI_TEST_LAUNCH_COUNT "${_launch_count}" PARENT_SCOPE) set(SD_MSI_TEST_SHELLEXT32 "${SD_MSI_SHELLEXT32}" PARENT_SCOPE) set(SD_MSI_TEST_X86_DLL "${SD_MSI_TEST_X86_DLL}" PARENT_SCOPE) +set(SD_MSI_TEST_UI "${_ui}" PARENT_SCOPE) diff --git a/packaging/msi/ShellDrive.wxs b/packaging/msi/ShellDrive.wxs index 70ddbb2..ec964ce 100644 --- a/packaging/msi/ShellDrive.wxs +++ b/packaging/msi/ShellDrive.wxs @@ -8,10 +8,12 @@ Defines: SD_VERSION, SD_FLAVOUR, SD_PRODUCT_CODE, SD_PACKAGE_CODE, SD_UPGRADE_CODE, SD_INSTALLER_VERSION, SD_PF_DIR (ProgramFilesFolder or ProgramFiles64Folder), SD_WIN64 (yes/no), SD_BIN_DIR, SD_ICON, - SD_LAUNCH (none or win10), SD_WITH_SERVICE (0 or 1, 0 until the service exists: M5, D6), SD_LICENSE_RTF (the + SD_LAUNCH (none, win10 or arm32), SD_WITH_SERVICE (0 or 1, 0 until the service exists: M5, D6), SD_LICENSE_RTF (the LICENSE of the repository converted to RTF by build-msi.cmake, shown by the license page), SD_SHELLEXT32 (1 on the x64 flavours: the x86-sse2 shell extension in INSTALLDIR†, Win64="no", 04 §8.3) and - SD_BIN32_DIR (the folder of that x86 DLL). + SD_BIN32_DIR (the folder of that x86 DLL), SD_UI (1: the UI, the shell extension and the Start menu shortcut; 0 in the + ARM32 package, which has the CLI and the host only, 04 §5.1) and SD_DOKANY (1: the options page offers Dokany; 0 in + the ARM32 package, which has no Dokany, 04 §7.1). --> @@ -75,9 +77,15 @@ + - + + + + + + @@ -94,6 +102,14 @@ "" AND SD_OSMAJOR >= 10)]]> + + + + + + "" AND SD_OSMAJOR >= 10 AND SD_OSBUILD <> "" AND SD_OSBUILD < 26100)]]> @@ -115,6 +131,7 @@ + @@ -122,6 +139,7 @@ + @@ -142,6 +160,7 @@ + @@ -152,6 +171,7 @@ + @@ -259,7 +279,9 @@ + + @@ -274,6 +296,7 @@ + @@ -287,6 +310,7 @@ + @@ -370,10 +394,12 @@ Text="!(loc.OptionsDescription)" /> + + -.msi), @@ -16,12 +16,14 @@ # SD_MSI_LICENSE (the LICENSE file of the repository: the license page of the dialog set), SD_MSIGEN, SD_MSIEMBED # (tool paths), SD_MSI_SHELLEXT32 (1 on the x64 flavours: the x86-sse2 shell extension goes to INSTALLDIR† and # is registered in the 32-bit view, 04 §8.3; 0 otherwise) and SD_MSI_BIN32 (the folder of that x86 DLL; any -# non-empty folder when SD_MSI_SHELLEXT32 is 0). +# non-empty folder when SD_MSI_SHELLEXT32 is 0), SD_MSI_UI (1: the package carries the UI, the shell extension +# and the Start menu shortcut; 0 on ARM32, CLI and host only, 04 §5.1) and SD_MSI_DOKANY (1: the options page +# offers the Dokany backend; 0 on ARM32, which has no Dokany, 04 §7.1). foreach(_v IN ITEMS SD_MSI_FLAVOUR SD_MSI_FAMILY SD_MSI_VERSION SD_MSI_ARCH SD_MSI_INSTALLER_VERSION SD_MSI_PF SD_MSI_WIN64 SD_MSI_LAUNCH SD_MSI_WITH_SERVICE SD_MSI_UPGRADE_CODE SD_MSI_BIN SD_MSI_ICON SD_MSI_SRC SD_MSI_GEN SD_MSI_OUT SD_MSI_REG SD_MSI_PO SD_MSI_WIX SD_MSI_LICENSE SD_MSIGEN SD_MSIEMBED - SD_MSI_SHELLEXT32 SD_MSI_BIN32) + SD_MSI_SHELLEXT32 SD_MSI_BIN32 SD_MSI_UI SD_MSI_DOKANY) if("${${_v}}" STREQUAL "") message(FATAL_ERROR "build-msi.cmake: ${_v} is not set") endif() @@ -93,9 +95,14 @@ if(SD_MSI_SHELLEXT32 STREQUAL "1") else() set(_shellext32_yes no) endif() +if(SD_MSI_UI STREQUAL "1") + set(_ui_yes yes) +else() + set(_ui_yes no) +endif() sd_msi_step("sd-msigen registry" "${SD_MSIGEN}" registry --in "${SD_MSI_REG}" --out "${_work}/Registry.wxi" --flavour "${SD_MSI_FLAVOUR}" --family "${SD_MSI_FAMILY}" --win64 "${SD_MSI_WIN64}" - --shellext32 "${_shellext32_yes}") + --shellext32 "${_shellext32_yes}" --ui "${_ui_yes}") # 2b. The license page (WixUILicenseRtf): the LICENSE file of the repository, converted to RTF for this build. The text # is the only source; no RTF copy is checked in. Backslash, braces and line ends are escaped as RTF needs. @@ -133,6 +140,7 @@ sd_msi_step("candle" "${_candle}" -nologo -arch "${SD_MSI_ARCH}" "-dSD_PF_DIR=${SD_MSI_PF}" "-dSD_WIN64=${SD_MSI_WIN64}" "-dSD_BIN_DIR=${SD_MSI_BIN}" "-dSD_ICON=${SD_MSI_ICON}" "-dSD_LAUNCH=${SD_MSI_LAUNCH}" "-dSD_WITH_SERVICE=${SD_MSI_WITH_SERVICE}" "-dSD_SHELLEXT32=${SD_MSI_SHELLEXT32}" "-dSD_BIN32_DIR=${SD_MSI_BIN32}" + "-dSD_UI=${SD_MSI_UI}" "-dSD_DOKANY=${SD_MSI_DOKANY}" "-dSD_LICENSE_RTF=${_license_rtf}" -out "${_obj}" "${SD_MSI_SRC}/ShellDrive.wxs") diff --git a/tests/msi/CMakeLists.txt b/tests/msi/CMakeLists.txt index fe7583b..3b745a9 100644 --- a/tests/msi/CMakeLists.txt +++ b/tests/msi/CMakeLists.txt @@ -3,28 +3,47 @@ # ctest --test-dir build/ -C Release -L msi # sd-test-msi reads the package through msi.dll (tables, transforms with the language check) and runs an # administrative install (msiexec /a: files only, no registry, service or custom action) into the build tree. +# +# An ARM64 or ARM32 tree builds sd-test-msi in its nested x64 build (cmake/SdHostTools.cmake), so its msi.* tests run on +# this host like the x86 and x64 ones; the ARM ctest presets select only them (CMakePresets.json). + +if(SD_HOST_TOOLS_ONLY) + # The nested x64 build of an ARM tree: the test tool only. + add_executable(sd-test-msi sd-test-msi.c) + target_link_libraries(sd-test-msi PRIVATE msi.lib) + set_target_properties(sd-test-msi PROPERTIES FOLDER "tests") + return() +endif() # Scratch folder of the administrative install (inside the build tree, removed with it). file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/msi-admin") -add_executable(sd-test-msi sd-test-msi.c) -target_link_libraries(sd-test-msi PRIVATE msi.lib) -set_target_properties(sd-test-msi PROPERTIES FOLDER "tests") -# The package must exist before these tests run: on the x64 flavours no setup payload pulls the msi target into the -# build, so the test tool depends on it. -if(TARGET msi) - add_dependencies(sd-test-msi msi) +if(SD_CROSS_HOST_TOOLS) + # sd-test-msi is the imported x64 tool. Nothing else pulls the package into an ARM build when the setup payload does + # not carry it, so this target does (the tests read the file). + add_custom_target(sd_msi_for_tests ALL) + add_dependencies(sd_msi_for_tests msi sd_host_tools) + set_target_properties(sd_msi_for_tests PROPERTIES FOLDER "tests") +else() + add_executable(sd-test-msi sd-test-msi.c) + target_link_libraries(sd-test-msi PRIVATE msi.lib) + set_target_properties(sd-test-msi PROPERTIES FOLDER "tests") + # The package must exist before these tests run: on the x64 flavours no setup payload pulls the msi target into the + # build, so the test tool depends on it. + if(TARGET msi) + add_dependencies(sd-test-msi msi) + endif() endif() add_test(NAME msi.${SD_FLAVOUR}.tables COMMAND sd-test-msi tables "${SD_MSI_PACKAGE}" "${SD_MSI_TEST_VERSION}" "${SD_MSI_TEST_UPGRADE}" "${SD_MSI_TEST_PLATFORM}" "${SD_MSI_TEST_INSTALLER}" "${SD_MSI_TEST_LAUNCH_COUNT}" - "${SD_MSI_TEST_SHELLEXT32}") + "${SD_MSI_TEST_SHELLEXT32}" "${SD_MSI_TEST_UI}") set_tests_properties(msi.${SD_FLAVOUR}.tables PROPERTIES LABELS msi TIMEOUT 300) add_test(NAME msi.${SD_FLAVOUR}.admin COMMAND sd-test-msi admin "${SD_MSI_PACKAGE}" "${CMAKE_BINARY_DIR}/msi-admin/${SD_FLAVOUR}" - "${SD_MSI_TEST_SHELLEXT32}") + "${SD_MSI_TEST_PLATFORM}" "${SD_MSI_TEST_SHELLEXT32}" "${SD_MSI_TEST_UI}") # msiexec runs one installation at a time per machine (Windows Installer service): windows_installer. set_tests_properties(msi.${SD_FLAVOUR}.admin PROPERTIES LABELS msi TIMEOUT 900 RESOURCE_LOCK windows_installer) @@ -35,19 +54,22 @@ add_test(NAME msi.${SD_FLAVOUR}.wixui -P "${CMAKE_CURRENT_SOURCE_DIR}/check-wixui.cmake") set_tests_properties(msi.${SD_FLAVOUR}.wixui PROPERTIES LABELS msi TIMEOUT 60) -# The 32-bit shell extension of the x64 packages (04 §6.2, §8.3): the x86-sse2 DLL of the nested x86 configure +# The 32-bit shell extension of the x64 and ARM64 packages (04 §6.2, §8.3): the x86-sse2 DLL of the nested x86 configure # (cmake/SdShellExtX86.cmake) has the rules of its own flavour. The nested build runs the same audits after its link; # these tests repeat them on the file the package carries, with the arguments of sd_add_import_audit and -# sd_add_isa_check (cmake/SdModule.cmake) for SD_FLAVOUR=x86-sse2. +# sd_add_isa_check (cmake/SdModule.cmake) for SD_FLAVOUR=x86-sse2. An ARM tree has no sd-isacheck (it audits x86 code +# only, 04 §2.4), so there only the nested build's own ISA audit covers the DLL. if(SD_MSI_TEST_SHELLEXT32) add_test(NAME importcheck.shelldrive-shellext.x86-sse2 COMMAND sd-importcheck --floor "${CMAKE_SOURCE_DIR}/tests/os-imports/xp-sp3-x86.txt" --subsystem 5.01 --forbid-cfg --require-safeseh "${SD_MSI_TEST_X86_DLL}") set_tests_properties(importcheck.shelldrive-shellext.x86-sse2 PROPERTIES LABELS importcheck) - add_test(NAME isacheck.shelldrive-shellext.x86-sse2 - COMMAND sd-isacheck --floor sse2 - --allow "${CMAKE_SOURCE_DIR}/tools/sd-isacheck/crt-allow.txt" - --allow "${CMAKE_SOURCE_DIR}/tools/sd-isacheck/hw-allow.txt" - "${SD_MSI_TEST_X86_DLL}") - set_tests_properties(isacheck.shelldrive-shellext.x86-sse2 PROPERTIES LABELS isacheck) + if(TARGET sd-isacheck) + add_test(NAME isacheck.shelldrive-shellext.x86-sse2 + COMMAND sd-isacheck --floor sse2 + --allow "${CMAKE_SOURCE_DIR}/tools/sd-isacheck/crt-allow.txt" + --allow "${CMAKE_SOURCE_DIR}/tools/sd-isacheck/hw-allow.txt" + "${SD_MSI_TEST_X86_DLL}") + set_tests_properties(isacheck.shelldrive-shellext.x86-sse2 PROPERTIES LABELS isacheck) + endif() endif() diff --git a/tests/msi/sd-test-msi.c b/tests/msi/sd-test-msi.c index 3de59be..52d0793 100644 --- a/tests/msi/sd-test-msi.c +++ b/tests/msi/sd-test-msi.c @@ -1,6 +1,6 @@ /* sd-test-msi: checks of a built per-machine MSI (docs/design/04 §8, CTest label msi). Opt-in (SD_BUILD_MSI=ON). * - * sd-test-msi tables + * sd-test-msi tables * Reads the package through msi.dll only (nothing is installed): * - ProductVersion, UpgradeCode, ProductName and Manufacturer of the Property table; * - the Template summary: the platform prefix, and every LCID of the language table is listed (C43); @@ -13,10 +13,14 @@ * - with shellext32 = 1 (the x64 flavours, 04 §8.3, 03 §8.5): the x86 shell extension component in * Program Files (x86)\LibreStatic\ShellDrive with Win64 = no, in the ShellExt feature, and its 32-bit * registry rows in components without the 64-bit attribute; with 0 there is no such component or row. - * sd-test-msi admin [shellext32] + * - with ui = 0 (the ARM32 package, 04 §5.1): no UI, shell extension or Start menu component, feature, file or + * registry row, no Dokany option, and the ARM32 launch condition (Windows 10 or later, build below 26100). + * is the Template prefix: Intel, x64, Arm64 or Arm. + * sd-test-msi admin * Administrative install (msiexec /a) into \target: no registry, service or system change. Checks - * that the four binaries are laid out and, with shellext32 = 1, that the x64 shell extension is there (a PE image - * of machine AMD64; the x86 copy has the same path in an admin image, see the admin check). + * that the binaries of the flavour are laid out (four, or the CLI and the host with ui = 0), that shelldrive.exe + * is a PE image of the platform's machine and, with shellext32 = 1, that the native shell extension is there + * (the x86 copy has the same path in an admin image, see the admin check). * The caller removes . * * Exit code 0 when every check passes; each check prints "ok" or "FAIL" with its name. @@ -314,7 +318,7 @@ static int count_files_containing(MSIHANDLE db, const wchar_t *text) } static int run_tables(const wchar_t *msi, const wchar_t *version, const wchar_t *upgrade, const wchar_t *platform, - int installer_version, int launch_count, int shellext32) + int installer_version, int launch_count, int shellext32, int ui) { MSIHANDLE db = 0; wchar_t buf[2048], tmpl[1024], copy[MAX_PATH], temp_dir[MAX_PATH]; @@ -322,10 +326,13 @@ static int run_tables(const wchar_t *msi, const wchar_t *version, const wchar_t unsigned i; int ival = 0, ok; const wchar_t *components[] = { - L"C_Core_Cli", L"C_Core_Host", L"C_Core_Ui", L"C_ShellExt", L"C_StartMenu", L"C_Ini_Update", - L"C_Ini_Backend", L"C_LinkedConn", L"C_Installer", L"C_Rm_Secrets", L"C_Rm_Keys", L"C_Rm_Data", L"SD_Reg_000" + L"C_Core_Cli", L"C_Core_Host", L"C_Ini_Update", L"C_Ini_Backend", L"C_LinkedConn", L"C_Installer", + L"C_Rm_Secrets", L"C_Rm_Keys", L"C_Rm_Data", L"SD_Reg_000" }; - const wchar_t *features[] = { L"ShellDrive", L"Core", L"ShellExt", L"StartMenu", L"Autostart" }; + const wchar_t *features[] = { L"ShellDrive", L"Core", L"Autostart" }; + /* The UI, the shell extension and the Start menu: in every package but ARM32 (04 §5.1). */ + const wchar_t *ui_components[] = { L"C_Core_Ui", L"C_ShellExt", L"C_StartMenu" }; + const wchar_t *ui_features[] = { L"ShellExt", L"StartMenu" }; const wchar_t *hooks[] = { L"CA_ProgramDataSecure", L"CA_WebDavTune_Apply", L"CA_WebDavTune_Revert", L"CA_SignalShutdown", L"CA_UserCleanup" }; const size_t nhooks = sizeof(hooks) / sizeof(hooks[0]); @@ -347,7 +354,7 @@ static int run_tables(const wchar_t *msi, const wchar_t *version, const wchar_t ok = summary_read(msi, PID_TEMPLATE_PROP, tmpl, 1024, NULL); check(ok && wcsncmp(tmpl, platform, wcslen(platform)) == 0 && tmpl[wcslen(platform)] == L';', - "Template platform prefix (Intel, x64, Arm64)"); + "Template platform prefix (Intel, x64, Arm64, Arm)"); ok = 1; for (i = 0; i < N_LCIDS; i++) { if (!template_lists(tmpl, g_lcids[i])) { @@ -405,13 +412,44 @@ static int run_tables(const wchar_t *msi, const wchar_t *version, const wchar_t } check(ok, "feature tree of 04 §8.3 present"); + ok = 1; + for (i = 0; i < sizeof(ui_components) / sizeof(ui_components[0]); i++) { + swprintf_s(sql, 512, L"SELECT `Component` FROM `Component` WHERE `Component` = '%ls'", ui_components[i]); + if (count_rows(db, sql) != (ui ? 1 : 0)) { + printf(" component %ls %s\n", ui_components[i], ui ? "missing" : "present"); + ok = 0; + } + } + for (i = 0; i < sizeof(ui_features) / sizeof(ui_features[0]); i++) { + swprintf_s(sql, 512, L"SELECT `Feature` FROM `Feature` WHERE `Feature` = '%ls'", ui_features[i]); + if (count_rows(db, sql) != (ui ? 1 : 0)) { + printf(" feature %ls %s\n", ui_features[i], ui ? "missing" : "present"); + ok = 0; + } + } + check(ok, ui ? "UI, shell extension and Start menu components and features present" + : "no UI, shell extension or Start menu component or feature (ARM32, 04 §5.1)"); + check(count_files_containing(db, L"shelldrive-ui.exe") == (ui ? 1 : 0) && + any_key_contains(db, L"App Paths\\shelldrive-ui.exe") == ui, + ui ? "the UI file and its App Paths row" : "no UI file and no App Paths row naming it (ARM32)"); + /* The Dokany option (04 §7.1): offered where a Dokany package exists, absent in ARM32, where WebDAV is the backend. */ + check(count_rows(db, L"SELECT `Control` FROM `Control` WHERE `Dialog_` = 'OptionsDlg' AND `Control` = 'DokanyCheckBox'") == ui && + query_first(db, L"SELECT `Value` FROM `Property` WHERE `Property` = 'SD_DOKANY_STATE'", buf, 2048) && + wcscmp(buf, ui ? L"1" : L"0") == 0, + ui ? "options page offers Dokany, on by default" : "no Dokany option, WebDAV backend by default (ARM32)"); + check(query_first(db, L"SELECT `Value` FROM `Registry` WHERE `Key` = 'Software\\Microsoft\\Windows\\CurrentVersion\\Run'" L" AND `Name` = 'ShellDrive'", buf, 2048), "HKLM Run value of the autostart feature"); check(query_first(db, L"SELECT `Value` FROM `Registry` WHERE `Key` = 'Software\\LibreStatic\\ShellDrive'" L" AND `Name` = 'Version'", buf, 2048), "LibreStatic Version marker for Intune detection (04 §8.3)"); - { + if (!ui) { + check(count_files_containing(db, L"shelldrive-shellext.dll") == 0 && !any_key_contains(db, L"ContextMenuHandlers") && + !any_key_contains(db, L"{C2A44993-862B-42AB-B40B-254EFB1F5C43}"), + "no shell extension file or registration row (ARM32)"); + } + if (ui) { /* The shell extension rows (packaging/registration.txt, feature shellext, 03 §8.5): the same keys and values as the per-user registration in apps/shelldrive-shellext/shellext_register.c, all in the ShellExt feature. */ static const struct { const wchar_t *key; const wchar_t *name; const wchar_t *value; } rows[] = { @@ -446,7 +484,7 @@ static int run_tables(const wchar_t *msi, const wchar_t *version, const wchar_t check(ok_values, "shell extension rows: CLSIDs with the DLL and Apartment, Drive/Directory/* and This PC handlers, Approved (03 §8.5)"); check(ok_feature, "shell extension rows are in the ShellExt feature (no registration without the DLL)"); } - { + if (ui) { /* The 32-bit view of the shell extension (04 §8.3, 03 §8.5): the x86 DLL in INSTALLDIR\x86 (Win64 = no) and the CLSID and handler rows of the same keys with the x86 path, in components without the 64-bit attribute. The 64-bit rows of the same keys stay in 64-bit components. With shellext32 = 0 the package has none of this. */ @@ -513,6 +551,29 @@ static int run_tables(const wchar_t *msi, const wchar_t *version, const wchar_t "no Uninstall key of ShellDrive from the registration rows (Windows Installer writes the ARP entry)"); check(count_rows(db, L"SELECT `Condition` FROM `LaunchCondition`") == launch_count, "launch conditions: MajorUpgrade downgrade check plus the OS check of the flavour (04 §8.2)"); + if (wcscmp(platform, L"Arm") == 0) { + /* ARM32 (04 §5.1): Windows 10 or later (RT 8.1 has no CurrentMajorVersionNumber) and a build below 26100 (Windows 11 + 24H2 runs no ARM32 app). */ + MSIHANDLE view = 0, rec = 0; + int found = 0; + check(count_rows(db, L"SELECT `Property` FROM `AppSearch` WHERE `Property` = 'SD_OSBUILD'") == 1 && + query_first(db, L"SELECT `Name` FROM `RegLocator` WHERE `Signature_` = 'SD_OSBUILD_RS'", buf, 2048) && + wcscmp(buf, L"CurrentBuildNumber") == 0, + "ARM32: SD_OSBUILD reads CurrentBuildNumber (04 §5.1)"); + if (MsiDatabaseOpenViewW(db, L"SELECT `Condition` FROM `LaunchCondition`", &view) == ERROR_SUCCESS) { + if (MsiViewExecute(view, 0) == ERROR_SUCCESS) { + while (!found && MsiViewFetch(view, &rec) == ERROR_SUCCESS) { + DWORD cch = 2048; + if (MsiRecordGetStringW(rec, 1, buf, &cch) == ERROR_SUCCESS) + found = contains_ci(buf, L"SD_OSMAJOR >= 10") && contains_ci(buf, L"SD_OSBUILD < 26100"); + MsiCloseHandle(rec); + rec = 0; + } + } + MsiCloseHandle(view); + } + check(found, "ARM32 launch condition: Windows 10 or later and a build below 26100 (Windows 11 24H2)"); + } check(count_rows(db, L"SELECT `Section` FROM `IniFile` WHERE `Section` = 'update'") >= 2, "settings.ini [update] lines for the public properties (04 §8.4)"); @@ -703,7 +764,21 @@ static int find_pe_suffix(const wchar_t *dir, const wchar_t *suffix, WORD machin return found; } -static int run_admin(const wchar_t *msi_in, const wchar_t *work_in, int shellext32) +/* PE machine of a Template platform prefix: i386, AMD64, ARM64, ARMNT (Thumb-2); 0 for an unknown prefix. */ +static WORD platform_machine(const wchar_t *platform) +{ + if (wcscmp(platform, L"Intel") == 0) + return 0x014c; + if (wcscmp(platform, L"x64") == 0) + return 0x8664; + if (wcscmp(platform, L"Arm64") == 0) + return 0xAA64; + if (wcscmp(platform, L"Arm") == 0) + return 0x01C4; + return 0; +} + +static int run_admin(const wchar_t *msi_in, const wchar_t *work_in, const wchar_t *platform, int shellext32, int ui) { wchar_t msi[MAX_PATH], work[MAX_PATH], target[MAX_PATH], copy[MAX_PATH], cmd[2048]; STARTUPINFOW si; @@ -711,6 +786,8 @@ static int run_admin(const wchar_t *msi_in, const wchar_t *work_in, int shellext DWORD code = 1; const wchar_t *files[] = { L"shelldrive.exe", L"shelldrive-host.exe", L"shelldrive-ui.exe", L"shelldrive-shellext.dll" }; + const unsigned nfiles = ui ? 4 : 2; /* ARM32: the CLI and the host only (04 §5.1) */ + WORD machine = platform_machine(platform); unsigned i; int ok = 1; @@ -748,19 +825,24 @@ static int run_admin(const wchar_t *msi_in, const wchar_t *work_in, int shellext check(code == 0, "admin install: msiexec exits 0"); for (i = 0; i < sizeof(files) / sizeof(files[0]); i++) { - if (!find_file(target, files[i])) { - printf(" %ls not copied\n", files[i]); + if (find_file(target, files[i]) != (i < nfiles)) { + printf(" %ls %s\n", files[i], i < nfiles ? "not copied" : "present in a package without it"); ok = 0; } } check(ok, "admin install: the binaries of the flavour are laid out"); + { + int native = machine != 0 && find_pe_suffix(target, L"\\LibreStatic\\ShellDrive\\shelldrive.exe", machine); + check(native, "admin install: shelldrive.exe is built for the package platform"); + ok = ok && native; + } if (shellext32) { /* msiexec /a lays the folders out below TARGETDIR without their root, so Program Files and Program Files (x86) - both become LibreStatic\ShellDrive here and the 32-bit copy is not visible. The x64 copy is checked by its PE + both become LibreStatic\ShellDrive here and the 32-bit copy is not visible. The native copy is checked by its PE machine type; the 32-bit copy is checked by its File, Component and Directory rows in the tables check. */ - int x64 = find_pe_suffix(target, L"\\LibreStatic\\ShellDrive\\shelldrive-shellext.dll", 0x8664); - check(x64, "admin install: the x64 shell extension is laid out (04 §8.3)"); - ok = ok && x64; + int native = find_pe_suffix(target, L"\\LibreStatic\\ShellDrive\\shelldrive-shellext.dll", machine); + check(native, "admin install: the native shell extension is laid out (04 §8.3)"); + ok = ok && native; } return ok && code == 0; } @@ -768,13 +850,14 @@ static int run_admin(const wchar_t *msi_in, const wchar_t *work_in, int shellext int wmain(int argc, wchar_t **argv) { int ok; - if (argc == 9 && wcscmp(argv[1], L"tables") == 0) { - ok = run_tables(argv[2], argv[3], argv[4], argv[5], _wtoi(argv[6]), _wtoi(argv[7]), _wtoi(argv[8])); - } else if ((argc == 4 || argc == 5) && wcscmp(argv[1], L"admin") == 0) { - ok = run_admin(argv[2], argv[3], argc == 5 && _wtoi(argv[4]) == 1); + if (argc == 10 && wcscmp(argv[1], L"tables") == 0) { + ok = run_tables(argv[2], argv[3], argv[4], argv[5], _wtoi(argv[6]), _wtoi(argv[7]), _wtoi(argv[8]), + _wtoi(argv[9]) == 1); + } else if (argc == 7 && wcscmp(argv[1], L"admin") == 0) { + ok = run_admin(argv[2], argv[3], argv[4], _wtoi(argv[5]) == 1, _wtoi(argv[6]) == 1); } else { - fprintf(stderr, "usage: sd-test-msi tables \n" - " sd-test-msi admin [shellext32]\n"); + fprintf(stderr, "usage: sd-test-msi tables \n" + " sd-test-msi admin \n"); return 2; } printf("%s\n", ok ? "PASS" : "FAILED"); diff --git a/tools/ci-local.ps1 b/tools/ci-local.ps1 index d1b8c59..5a42909 100644 --- a/tools/ci-local.ps1 +++ b/tools/ci-local.ps1 @@ -11,8 +11,9 @@ arm64 (04 §1.1, M6) and arm32 (04 §1.1, §5.1, D5) are optional, build-only presets: they are not in the default list. Each cross-compiles Release and Debug on this x64 machine (the build-time tools come from a nested x64 build) - and runs no tests, because ARM binaries cannot run here (04 §11.4). Run them with -Presets arm64 or -Presets arm32, - or with the default list plus the ARM preset. + and runs no unit tests, because ARM binaries cannot run here (04 §11.4). With WiX 3.14 installed they also build + their MSI (D31) and run its msi.* checks in Release: the check tool is the x64 one of the nested build. Run them + with -Presets arm64 or -Presets arm32, or with the default list plus the ARM preset. .EXAMPLE powershell -ExecutionPolicy Bypass -File tools\ci-local.ps1 @@ -88,6 +89,9 @@ if (-not $NoFixture -and -not $BuildOnly) { } } +# WiX 3.14 present: the MSI presets below configure with SD_BUILD_MSI. +$wix = Test-Path (Join-Path ${env:ProgramFiles(x86)} 'WiX Toolset v3.14\bin\candle.exe') + $results = @() foreach ($preset in $Presets) { $sw = [Diagnostics.Stopwatch]::StartNew() @@ -115,9 +119,9 @@ foreach ($preset in $Presets) { if (-not $TestOnly) { Write-Host "== [$preset] configure" # The per-machine MSI (04 section 8) and its table/admin-image tests are built on one x86 and one x64 flavour - # when WiX 3.14 is installed; without this the msi.* tests are not even registered. + # and on both ARM flavours (D31) when WiX 3.14 is installed; without this the msi.* tests are not even registered. $cfgArgs = @('--preset', $preset) - if ($preset -in @('x86-ia32', 'x64-modern') -and (Test-Path (Join-Path ${env:ProgramFiles(x86)} 'WiX Toolset v3.14\bin\candle.exe'))) { + if ($preset -in @('x86-ia32', 'x64-modern', 'arm64', 'arm32') -and $wix) { $cfgArgs += '-DSD_BUILD_MSI=ON' } & $cmake @cfgArgs | Out-Host @@ -139,8 +143,9 @@ foreach ($preset in $Presets) { if ($warn) { throw "build $cfg reported $warn warnings/errors" } } if ($BuildOnly) { continue } - if ($preset -in @('arm64', 'arm32')) { - # Build only: ARM test binaries cannot run on this x64 host (04 §11.4); the ARM ctest presets select no tests, so ctest is skipped. + if ($preset -in @('arm64', 'arm32') -and (-not $wix -or $cfg -like '*-debug')) { + # Build only: ARM test binaries cannot run on this x64 host (04 §11.4). The ARM ctest presets select only the + # msi.* checks (x64 tool, Release), so without WiX or in Debug ctest is skipped. $note += "$preset $cfg built, not tested (build only); " continue } diff --git a/tools/sd-msigen/sd-msigen.c b/tools/sd-msigen/sd-msigen.c index 8f9667f..70299cd 100644 --- a/tools/sd-msigen/sd-msigen.c +++ b/tools/sd-msigen/sd-msigen.c @@ -6,11 +6,13 @@ * (04 §12.2). UpgradeCodes are not derived: they are fixed in 04 §8.2. * * sd-msigen registry --in --out --flavour - * --family --win64 yes|no --shellext32 yes|no + * --family --win64 yes|no --shellext32 yes|no --ui yes|no * Turns the registry rows into a WiX include. Every row becomes one Component with one RegistryValue * (HKLM); the components are grouped by feature into SD_RegCore (core), SD_RegAutostart (autostart), * SD_RegShellExt (shellext, referenced by the ShellExt feature) and SD_RegShellExt32 (shellext32: the 32-bit * view of the shell extension, Win64="no", written only with --shellext32 yes, 04 §8.3, 03 §8.5). + * --ui no (the ARM32 package: CLI and host only, 04 §5.1) leaves out the shellext and shellext32 rows and every + * row whose data names shelldrive-ui.exe (App Paths): the package has neither file. * The rows of Software\Microsoft\Windows\CurrentVersion\Uninstall\ShellDrive are skipped: Windows Installer * writes the Add/Remove Programs entry of an MSI itself (04 §6.7). [VERSION] becomes [ProductVersion], * [INSTALLDIR] stays (MSI formats it), [INSTALLDIR32] becomes [SD_INSTALLDIR32] (the 32-bit folder, Program @@ -228,6 +230,7 @@ static int cmd_registry(int argc, char **argv) const char *family = need_arg(argc, argv, "--family"); const char *win64 = need_arg(argc, argv, "--win64"); const char *shellext32 = need_arg(argc, argv, "--shellext32"); + const char *ui = need_arg(argc, argv, "--ui"); FILE *f, *o; char *line; reg_item *items = NULL; @@ -238,6 +241,10 @@ static int cmd_registry(int argc, char **argv) die("--win64 must be yes or no%s", NULL); if (strcmp(shellext32, "yes") != 0 && strcmp(shellext32, "no") != 0) die("--shellext32 must be yes or no%s", NULL); + if (strcmp(ui, "yes") != 0 && strcmp(ui, "no") != 0) + die("--ui must be yes or no%s", NULL); + if (strcmp(ui, "no") == 0 && strcmp(shellext32, "yes") == 0) + die("--shellext32 yes needs --ui yes%s", NULL); f = fopen(in, "rb"); if (!f) die("cannot open %s", in); @@ -260,6 +267,12 @@ static int cmd_registry(int argc, char **argv) free(line); continue; } + if (strcmp(ui, "no") == 0 && + (strcmp(item.feature, "shellext") == 0 || strstr(item.data, "shelldrive-ui.exe") != NULL)) { + /* no UI and no shell extension in this package: their rows would point at missing files */ + free(line); + continue; + } if (count == cap) { size_t ncap = cap ? cap * 2 : 32; reg_item *nitems = (reg_item *)xmalloc(ncap * sizeof(reg_item)); From 9a9485a8e78c7d09bc5bfc1742a6870a04a20d17 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 30 Oct 2026 17:25:44 -0300 Subject: [PATCH 3/3] ci: build the MSIs and run their checks on x86-ia32, x64-modern, arm64, arm32 A matrix include marks the four MSI entries. Each one checks for WiX 3.14 on the runner, configures with SD_BUILD_MSI=ON, runs the msi.* checks in Release and uploads the package. On arm64 and arm32 these checks are the only Release tests. x64-modern and arm64 now also install v141_xp, which the x86-sse2 shell extension in their MSI needs. --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++---- docs/dev/m4-notes.md | 6 ++++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5858a17..258bca6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ # runner (Dokany tests skip there). The arm64 and arm32 entries are build-only (M6, 04 §1.1, §5.1): they # cross-compile on the x64 runner, the build-time tools run from a nested x64 build, and no test runs (ARM # binaries cannot run there; the ARM64 test job belongs to windows-11-arm, §11.2; arm32 has no test job, D5). +# The per-machine MSIs (04 §8, D31) are built on x86-ia32, x64-modern, arm64 and arm32 with WiX 3.14 of the runner image, +# and their msi.* checks run there; on the ARM entries those are the only tests (x64 sd-test-msi from the nested +# host-tools build, CMakePresets.json). The x64 and ARM64 MSIs carry the x86-sse2 shell extension, built with v141_xp. # TM-R39: first-party actions only, pinned by commit SHA; read-only token. name: ci @@ -28,21 +31,44 @@ jobs: fail-fast: false matrix: preset: [x64-modern, x64-xp, x86-ia32, x86-sse2, arm64, arm32] + include: + - preset: x64-modern + msi: true + - preset: x86-ia32 + msi: true + - preset: arm64 + msi: true + - preset: arm32 + msi: true env: SD_BUILD_NUMBER: ${{ github.run_number }} + SD_MSI: ${{ matrix.msi }} steps: - name: Check out uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # The XP presets need it, and so do x64-modern and arm64 for the x86 shell extension of their MSI. - name: Set up v141_xp toolset - if: matrix.preset != 'x64-modern' && matrix.preset != 'arm64' && matrix.preset != 'arm32' + if: matrix.preset != 'arm32' uses: ./.github/actions/setup-xp-toolset + - name: Check WiX 3.14 + if: matrix.msi + shell: pwsh + run: | + $wix = if ($env:WIX) { $env:WIX } else { "${env:ProgramFiles(x86)}\WiX Toolset v3.14" } + if (-not (Test-Path (Join-Path $wix 'bin\candle.exe'))) { throw "WiX 3.14 not found at $wix (04 section 8.1)" } + Write-Host "WiX: $wix" + - name: Configure shell: pwsh - run: cmake --preset ${{ matrix.preset }} "-DSD_BUILD_NUMBER=${{ github.run_number }}" + run: | + $msiArgs = @() + if ($env:SD_MSI -eq 'true') { $msiArgs += '-DSD_BUILD_MSI=ON' } + cmake --preset ${{ matrix.preset }} "-DSD_BUILD_NUMBER=${{ github.run_number }}" @msiArgs + if ($LASTEXITCODE) { exit $LASTEXITCODE } # Release and Debug both run the post-build import audit (sd-importcheck) and the sdl10n # completeness check; either failure fails the build. @@ -54,9 +80,10 @@ jobs: shell: pwsh run: cmake --build --preset ${{ matrix.preset }}-debug - # arm64 and arm32 are build-only (ARM test binaries cannot run on the x64 runner, 04 §11.4): ctest is skipped. + # arm64 and arm32 run no unit tests (ARM test binaries cannot run on the x64 runner, 04 §11.4): their Release + # ctest preset selects only the msi.* checks, and their Debug ctest is skipped. - name: Test Release - if: matrix.preset != 'arm64' && matrix.preset != 'arm32' + if: (matrix.preset != 'arm64' && matrix.preset != 'arm32') || matrix.msi shell: pwsh run: ctest --preset ${{ matrix.preset }} --output-on-failure --output-junit "${{ github.workspace }}/build/ctest-${{ matrix.preset }}.xml" @@ -72,6 +99,7 @@ jobs: path: | build/${{ matrix.preset }}/bin/Release/*.exe build/${{ matrix.preset }}/bin/Release/*.pdb + build/${{ matrix.preset }}/msi/Release/*.msi build/ctest-${{ matrix.preset }}.xml if-no-files-found: error diff --git a/docs/dev/m4-notes.md b/docs/dev/m4-notes.md index 0adcd72..05ead55 100644 --- a/docs/dev/m4-notes.md +++ b/docs/dev/m4-notes.md @@ -897,5 +897,7 @@ Open (lab or later): resolve to. - The ARM64 package carries no x64 shell extension for x64 apps under emulation (04 §11.4 row "11 ARM64"). - The setup EXE's own ARM32 24H2/RT refusal (04 §5.1) is not implemented. -- GitHub CI does not build any MSI (`ci.yml` passes no `SD_BUILD_MSI`); the release pipeline's `package` job is still - a stub. +- The release pipeline's `package` job is still a stub. GitHub CI (`ci.yml`) builds the MSIs on `x86-ia32`, + `x64-modern`, `arm64` and `arm32` with the runner's WiX 3.14, runs their `msi.*` checks and uploads the packages. + The `x64-modern` and `arm64` jobs now also install `v141_xp` for the x86 shell extension, which adds several + minutes to each.