Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/@winglang/sdk/src/shared/bundling.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { execFileSync } from "child_process";
import * as crypto from "crypto";
import { mkdirSync, realpathSync, statSync, writeFileSync } from "fs";
import { stat } from "fs/promises";
Expand Down Expand Up @@ -132,6 +133,32 @@ export function createBundle(
};
}

/**
* Archives (zips) the contents of `srcDir` into a single zip file at `destFile`.
*
* This replaces cdktf's `TerraformAsset` with `AssetType.ARCHIVE`, whose internal
* implementation shells out to `node .../private/fs.js <src> <dest>` using a
* command string that does NOT quote the paths. When the project directory (or
* any path component) contains spaces, the shell splits on the spaces and the
* archive is written to the wrong location (or fails entirely), which later
* causes errors like `opening S3 object source (assets/.../archive.zip): no
* such file or directory` during `terraform apply`.
*
* Instead of relying on that shell string, we invoke the archiver directly with
* `execFileSync` and an array of arguments (no shell), so paths containing
* spaces are handled correctly. The resulting zip file is then presented to
* `TerraformAsset` as a single `AssetType.FILE`, which is simply copied into the
* output directory with `copyFileSync` (no shell involved).
*/
export function createArchive(srcDir: string, destFile: string): void {
// Reuse the archiver bundled with cdktf (this repo pins cdktf to an exact
// version, so this internal module path is stable). It is invoked as a CLI
// with array arguments rather than a shell string to preserve spaces.
// eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-require-imports
const archiveScript = require.resolve("cdktf/lib/private/fs") as string;
execFileSync(process.execPath, [archiveScript, srcDir, destFile]);
}

export interface SourceMap {
sourceRoot?: string;
sources: string[];
Expand Down
13 changes: 10 additions & 3 deletions packages/@winglang/sdk/src/target-tf-aws/function.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { resolve } from "path";
import { AssetType, Lazy, TerraformAsset } from "cdktf";
import { Construct } from "constructs";
import { App } from "./app";
Expand All @@ -11,7 +12,7 @@ import { S3Object } from "../.gen/providers/aws/s3-object";
import { SecurityGroup } from "../.gen/providers/aws/security-group";
import * as cloud from "../cloud";
import { NotImplementedError } from "../core/errors";
import { createBundle } from "../shared/bundling";
import { createArchive, createBundle } from "../shared/bundling";
import { DEFAULT_MEMORY_SIZE } from "../shared/function";
import { NameOptions, ResourceNames } from "../shared/resource-names";
import {
Expand Down Expand Up @@ -272,11 +273,17 @@ export class Function extends AwsFunction {

const bundle = createBundle(this.entrypoint, externalLibraries);

// Archive the bundle directory into a zip ourselves, then reference it as a
// single file. This avoids cdktf's `AssetType.ARCHIVE` shelling out with an
// unquoted path, which breaks when the directory contains spaces.
const archiveZip = resolve(bundle.directory, "..", "archive.zip");
createArchive(bundle.directory, archiveZip);

// would prefer to create TerraformAsset in the constructor, but using a CDKTF token for
// the "path" argument isn't supported
const asset = new TerraformAsset(this, "Asset", {
path: bundle.directory,
type: AssetType.ARCHIVE,
path: archiveZip,
type: AssetType.FILE,
});

this.bundleHash = bundle.hash;
Expand Down
10 changes: 7 additions & 3 deletions packages/@winglang/sdk/src/target-tf-azure/function.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from "fs";
import { resolve } from "path";
import { AssetType, Lazy, TerraformAsset } from "cdktf";
import { Construct } from "constructs";
import { App } from "./app";
Expand All @@ -14,7 +15,7 @@ import { StorageBlob } from "../.gen/providers/azurerm/storage-blob";
import * as cloud from "../cloud";
import { LiftMap } from "../core";
import { NotImplementedError } from "../core/errors";
import { createBundle } from "../shared/bundling";
import { createArchive, createBundle } from "../shared/bundling";
import {
CaseConventions,
NameOptions,
Expand Down Expand Up @@ -250,9 +251,12 @@ export class Function extends cloud.Function {
);

// Create zip asset from function code
const archiveZip = resolve(codeDir, "..", "archive.zip");
createArchive(codeDir, archiveZip);

const asset = new TerraformAsset(this, "Asset", {
path: `${codeDir}`,
type: AssetType.ARCHIVE,
path: archiveZip,
type: AssetType.FILE,
});

this.assetPath = asset.path;
Expand Down
11 changes: 7 additions & 4 deletions packages/@winglang/sdk/src/target-tf-gcp/function.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { writeFileSync } from "fs";
import { join, basename } from "path";
import { join, basename, resolve } from "path";
import { AssetType, Lazy, TerraformAsset, Fn } from "cdktf";
import { Construct } from "constructs";
import { App } from "./app";
Expand All @@ -13,7 +13,7 @@ import { StorageBucketObject } from "../.gen/providers/google/storage-bucket-obj
import * as cloud from "../cloud";
import { LiftMap } from "../core";
import { NotImplementedError } from "../core/errors";
import { createBundle } from "../shared/bundling";
import { createArchive, createBundle } from "../shared/bundling";
import { DEFAULT_MEMORY_SIZE } from "../shared/function";
import {
CaseConventions,
Expand Down Expand Up @@ -223,9 +223,12 @@ export class Function extends cloud.Function {
),
);

const archiveZip = resolve(bundle.directory, "..", "archive.zip");
createArchive(bundle.directory, archiveZip);

const asset = new TerraformAsset(this, "Asset", {
path: bundle.directory,
type: AssetType.ARCHIVE,
path: archiveZip,
type: AssetType.FILE,
});

this.assetPath = asset.path;
Expand Down
33 changes: 32 additions & 1 deletion packages/@winglang/sdk/test/shared/bundling.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
import {
writeFileSync,
mkdtempSync,
readFileSync,
mkdirSync,
existsSync,
} from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { describe, it, expect } from "vitest";
import { encode, decode } from "vlq";
import { fixSourcemaps } from "../../src/shared/bundling";
import { createArchive, fixSourcemaps } from "../../src/shared/bundling";

describe("createArchive", () => {
it("should create a zip archive when the directory path contains spaces", () => {
// create a temp directory with spaces in its name, mirroring the scenario in
// https://github.com/winglang/wing/issues/6465 where the project directory
// contains a space
const root = mkdtempSync(join(tmpdir(), "dir with spaces "));
const srcDir = join(root, "src");
const destFile = join(root, "archive.zip");
mkdirSync(srcDir, { recursive: true });
writeFileSync(join(srcDir, "index.js"), "module.exports = {};");

// WHEN
createArchive(srcDir, destFile);

// THEN
expect(existsSync(destFile)).toBe(true);
// a valid zip archive starts with the "PK" magic bytes
expect(readFileSync(destFile).subarray(0, 2).toString()).toBe("PK");
expect(readFileSync(destFile).length).toBeGreaterThan(0);
});
});

describe("fixSourcemaps", () => {
it("should fix sourcemaps", () => {
Expand Down
Loading