mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(artifacts): make extraction output tree work on Windows (#753)
SafeOutputTree.create() called fchmod(0o700) on a directory handle and
commit() fsynced a directory handle. Both are POSIX-only: Windows rejects
them with EPERM ("operation not permitted, fchmod"/"fsync"), so every
extract_artifact call failed with a generic io error and produced no output.
Skip the redundant directory chmod and directory fsync on Windows. mkdir
already applies the requested mode, and file contents are synced in write().
Add a Windows regression test and run it, plus the archive-safety test, in
the curated Windows CI lane so the behavior is covered.
This commit is contained in:
@@ -365,6 +365,8 @@ jobs:
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
tests/boundary/filesystem/binaryTarget.test.ts
|
||||
tests/boundary/filesystem/safeOutputTree.test.ts
|
||||
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
|
||||
tests/boundary/filesystem/setupUnavailableProvider.test.ts
|
||||
tests/boundary/filesystem/asarInventoryFreshness.test.ts
|
||||
tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts
|
||||
|
||||
@@ -43,12 +43,24 @@ export class SafeOutputTree {
|
||||
status: "not-required",
|
||||
};
|
||||
|
||||
private constructor(outputRoot: string) {
|
||||
private constructor(
|
||||
outputRoot: string,
|
||||
private readonly platform: NodeJS.Platform,
|
||||
) {
|
||||
this.#outputRoot = outputRoot;
|
||||
}
|
||||
|
||||
/** Exclusively create the absent destination as this operation's owned tree. */
|
||||
static async create(outputRoot: string): Promise<SafeOutputTree> {
|
||||
/**
|
||||
* Exclusively create the absent destination as this operation's owned tree.
|
||||
*
|
||||
* POSIX mode bits and directory `fchmod`/`fsync` have no Windows equivalent:
|
||||
* `mkdir` already applies the requested mode, so the redundant handle
|
||||
* `chmod` is skipped there to avoid an `EPERM` on the directory descriptor.
|
||||
*/
|
||||
static async create(
|
||||
outputRoot: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): Promise<SafeOutputTree> {
|
||||
if (!isAbsolute(outputRoot))
|
||||
throw new ArtifactReaderFailure(
|
||||
"path",
|
||||
@@ -78,16 +90,18 @@ export class SafeOutputTree {
|
||||
throw cause;
|
||||
});
|
||||
try {
|
||||
const stagingHandle = await open(
|
||||
canonicalOutput,
|
||||
constants.O_RDONLY | constants.O_DIRECTORY,
|
||||
);
|
||||
try {
|
||||
await stagingHandle.chmod(0o700);
|
||||
} finally {
|
||||
await stagingHandle.close();
|
||||
if (platform !== "win32") {
|
||||
const stagingHandle = await open(
|
||||
canonicalOutput,
|
||||
constants.O_RDONLY | constants.O_DIRECTORY,
|
||||
);
|
||||
try {
|
||||
await stagingHandle.chmod(0o700);
|
||||
} finally {
|
||||
await stagingHandle.close();
|
||||
}
|
||||
}
|
||||
return new SafeOutputTree(canonicalOutput);
|
||||
return new SafeOutputTree(canonicalOutput, platform);
|
||||
} catch (cause: unknown) {
|
||||
await rm(canonicalOutput, { recursive: true, force: true });
|
||||
throw cause;
|
||||
@@ -162,6 +176,11 @@ export class SafeOutputTree {
|
||||
/** Sync the owned output tree and prevent further writes through this instance. */
|
||||
async commit(): Promise<void> {
|
||||
this.#assertWritable();
|
||||
// Windows has no directory fsync; file contents are already synced in write().
|
||||
if (this.platform === "win32") {
|
||||
this.#published = true;
|
||||
return;
|
||||
}
|
||||
const parent = await open(
|
||||
dirname(this.#outputRoot),
|
||||
constants.O_RDONLY | constants.O_DIRECTORY,
|
||||
|
||||
@@ -81,4 +81,18 @@ describe("safe artifact output tree", () => {
|
||||
residualPaths: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("publishes without POSIX-only directory chmod or fsync on Windows", async () => {
|
||||
const parent = await createTestTempDirectory("rea-safe-output-");
|
||||
const output = join(parent, "published");
|
||||
const tree = await SafeOutputTree.create(output, "win32");
|
||||
const bytes = Buffer.from("windows output");
|
||||
const digest = createHash("sha256").update(bytes).digest("hex");
|
||||
|
||||
await tree.write("nested/file.txt", Readable.from(bytes), digest);
|
||||
await tree.commit();
|
||||
expect(await readFile(join(output, "nested", "file.txt"), "utf8")).toBe(
|
||||
"windows output",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user