microsoft/TypeAgent

Public

mirrored from https://github.com/microsoft/TypeAgentAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e4378ec2bd9df3e8a22320aab2c7ea3fbd088ff4

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

ts/tools/scripts/build-msi.mjs

300lines · modecode

1#!/usr/bin/env node
2// Copyright (c) Microsoft Corporation.
3// Licensed under the MIT License.
4
5// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
6
7/**
8 * build-msi.mjs
9 *
10 * Orchestrates the TypeAgent MSI build:
11 * 1. Download agent-server.<rid> artifact from ADO feed
12 * 2. Download typeagent-copilot-plugin artifact from ADO feed
13 * 3. Generate marketplace.json for Copilot CLI plugin registration
14 * 4. Harvest file components with heat.exe (one pass per artifact dir)
15 * 5. Compile WiX (candle) + link to MSI (light)
16 *
17 * Usage:
18 * node build-msi.mjs --rid win32-x64 --version 0.0.1-12345 --output ./out
19 * node build-msi.mjs --rid win32-x64 --version 0.0.1-12345 --plugin-version 0.0.1-12345
20 * node build-msi.mjs --skip-download --version 0.0.1-test --output ./out
21 */
22
23import fs from "node:fs";
24import path from "node:path";
25import { spawnSync } from "node:child_process";
26import { fileURLToPath } from "node:url";
27
28const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
30// ── Argument parsing ──────────────────────────────────────────────────────────
31const args = process.argv.slice(2);
32let rid = "win32-x64";
33let version = "latest";
34let pluginVersion = "latest";
35let outputDir = "./msi-out";
36let skipDownload = false;
37
38for (let i = 0; i < args.length; i++) {
39 if (args[i] === "--rid") rid = args[++i];
40 else if (args[i] === "--version") version = args[++i];
41 else if (args[i] === "--plugin-version") pluginVersion = args[++i];
42 else if (args[i] === "--output") outputDir = args[++i];
43 else if (args[i] === "--skip-download") skipDownload = true;
44}
45
46console.log(`📦 Building TypeAgent MSI`);
47console.log(` RID: ${rid}`);
48console.log(` Agent version: ${version}`);
49console.log(` Plugin version: ${pluginVersion}`);
50console.log(` Output: ${outputDir}`);
51
52// ── Paths ─────────────────────────────────────────────────────────────────────
53const wxsDir = path.resolve(__dirname, "../installers/wix");
54const wxsFile = path.join(wxsDir, "TypeAgent-AgentServer.wxs");
55const outputPath = path.resolve(outputDir);
56const agentArtifactDir = path.join(outputPath, "artifact", "agent-server");
57const pluginArtifactDir = path.join(outputPath, "artifact", "copilot-plugin");
58const marketplaceDir = path.join(outputPath, "marketplace");
59const agentHeatFile = path.join(outputPath, "AgentServerFiles.wxs");
60const pluginHeatFile = path.join(outputPath, "CopilotPluginFiles.wxs");
61
62if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath, { recursive: true });
63
64// ── Helpers ───────────────────────────────────────────────────────────────────
65function runCommand(cmd, cmdArgs, options = {}) {
66 console.log(`\n▶ ${cmd} ${cmdArgs.join(" ")}`);
67 const result = spawnSync(cmd, cmdArgs, {
68 stdio: "inherit",
69 shell: false,
70 ...options,
71 });
72 if (result.error) {
73 console.error(`❌ Command failed: ${result.error.message}`);
74 process.exit(1);
75 }
76 if (result.status !== 0) {
77 console.error(`❌ Command exited with code ${result.status}`);
78 process.exit(1);
79 }
80 return result;
81}
82
83function findExe(candidates) {
84 return candidates.find((p) => fs.existsSync(p)) ?? null;
85}
86
87function discoverWixBinDirs() {
88 const roots = ["C:\\Program Files (x86)", "C:\\Program Files"];
89 const dirs = [];
90 for (const root of roots) {
91 if (!fs.existsSync(root)) continue;
92 for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
93 if (!entry.isDirectory()) continue;
94 if (/^WiX Toolset v3(\.|$)/i.test(entry.name)) {
95 const bin = path.join(root, entry.name, "bin");
96 if (fs.existsSync(bin)) dirs.push(bin);
97 }
98 }
99 }
100 return dirs;
101}
102
103const WIX_PATHS = discoverWixBinDirs();
104
105function wixTool(name) {
106 if (WIX_PATHS.length === 0) {
107 console.error(
108 "❌ No WiX Toolset v3.x installation found. Install from https://github.com/wixtoolset/wix3/releases",
109 );
110 process.exit(1);
111 }
112 const found = findExe(WIX_PATHS.map((d) => path.join(d, name)));
113 if (!found) {
114 console.error(`❌ ${name} not found in: ${WIX_PATHS.join(", ")}`);
115 process.exit(1);
116 }
117 return found;
118}
119
120function downloadArtifact(packageName, ver, targetDir) {
121 if (fs.existsSync(targetDir)) fs.rmSync(targetDir, { recursive: true });
122 fs.mkdirSync(targetDir, { recursive: true });
123
124 const verArg = ver === "latest" ? "*" : ver;
125 runCommand("az", [
126 "artifacts",
127 "universal",
128 "download",
129 "--organization",
130 "https://dev.azure.com/msctoproj",
131 "--project",
132 "AI_Systems",
133 "--scope",
134 "project",
135 "--feed",
136 "typeagent",
137 "--name",
138 packageName,
139 "--version",
140 verArg,
141 "--path",
142 targetDir,
143 ]);
144
145 const files = fs.readdirSync(targetDir);
146 if (files.length === 0) {
147 console.error(`❌ Artifact download failed: ${targetDir} is empty`);
148 process.exit(1);
149 }
150 console.log(`✅ Downloaded ${packageName}: ${files.length} items`);
151}
152
153// ── Step 1: Download artifacts ────────────────────────────────────────────────
154if (!skipDownload) {
155 console.log(`\n📥 Downloading agent-server.${rid}...`);
156 downloadArtifact(`agent-server.${rid}`, version, agentArtifactDir);
157
158 console.log(`\n📥 Downloading typeagent-copilot-plugin...`);
159 downloadArtifact(
160 "typeagent-copilot-plugin",
161 pluginVersion,
162 pluginArtifactDir,
163 );
164} else {
165 for (const [label, dir] of [
166 ["agent-server", agentArtifactDir],
167 ["copilot-plugin", pluginArtifactDir],
168 ]) {
169 if (!fs.existsSync(dir)) {
170 console.error(
171 `❌ --skip-download set but ${label} dir not found: ${dir}`,
172 );
173 process.exit(1);
174 }
175 console.log(`⏭️ Skipping download, using: ${dir}`);
176 }
177}
178
179// ── Step 2: Generate marketplace.json ─────────────────────────────────────────
180console.log(`\n📝 Generating marketplace.json...`);
181fs.mkdirSync(marketplaceDir, { recursive: true });
182const semverVersion =
183 version
184 .replace(/[^0-9.]/g, ".")
185 .replace(/\.{2,}/g, ".")
186 .replace(/\.$/, "") || "0.0.1";
187const marketplace = {
188 name: "typeagent-local",
189 owner: { name: "Microsoft", email: "typeagent@microsoft.com" },
190 metadata: {
191 description: "TypeAgent Copilot CLI plugin",
192 version: semverVersion,
193 },
194 plugins: [
195 {
196 name: "typeagent",
197 description: "TypeAgent integration for Copilot CLI",
198 version: semverVersion,
199 source: "./copilot-plugin",
200 },
201 ],
202};
203fs.writeFileSync(
204 path.join(marketplaceDir, "marketplace.json"),
205 JSON.stringify(marketplace, null, 2),
206);
207console.log(`✅ Generated marketplace.json`);
208
209// ── Step 3: Harvest file components with heat.exe ─────────────────────────────
210const heatExe = wixTool("heat.exe");
211const candleExe = wixTool("candle.exe");
212const lightExe = wixTool("light.exe");
213
214function runHeat(dir, componentGroup, dirRef, varName, outFile) {
215 console.log(`\n🔥 Harvesting ${componentGroup} from ${dir}...`);
216 runCommand(heatExe, [
217 "dir",
218 dir,
219 "-cg",
220 componentGroup,
221 "-dr",
222 dirRef,
223 "-var",
224 `var.${varName}`,
225 "-gg", // generate stable GUIDs per file path
226 "-srd", // suppress root directory element
227 "-sfrag", // suppress fragment wrapping (use our own Product.wxs structure)
228 "-indent",
229 "2",
230 "-o",
231 outFile,
232 ]);
233 console.log(`✅ Harvested: ${outFile}`);
234}
235
236runHeat(
237 agentArtifactDir,
238 "AgentServerComponents",
239 "INSTALLFOLDER",
240 "AgentServerArtifactDir",
241 agentHeatFile,
242);
243runHeat(
244 pluginArtifactDir,
245 "CopilotPluginComponents",
246 "PLUGINFOLDER",
247 "CopilotPluginArtifactDir",
248 pluginHeatFile,
249);
250
251// ── Step 4: Compile WiX (candle.exe) ─────────────────────────────────────────
252console.log(`\n🕯️ Compiling WiX...`);
253
254const wixobjDir = outputPath;
255runCommand(candleExe, [
256 `-dProductVersion=${version}`,
257 `-dAgentServerArtifactDir=${agentArtifactDir}`,
258 `-dCopilotPluginArtifactDir=${pluginArtifactDir}`,
259 `-dMarketplaceDir=${marketplaceDir}`,
260 `-dInstallerSourceDir=${wxsDir}`,
261 `-arch`,
262 `x64`,
263 `-o`,
264 `${wixobjDir}\\`,
265 wxsFile,
266 agentHeatFile,
267 pluginHeatFile,
268]);
269console.log(`✅ Compiled WiX objects`);
270
271// ── Step 5: Link MSI (light.exe) ──────────────────────────────────────────────
272console.log(`\n💡 Linking MSI...`);
273
274const msiName = `TypeAgent-${version}-${rid}.msi`;
275const msiOutputPath = path.join(outputPath, msiName);
276
277runCommand(lightExe, [
278 `-ext`,
279 `WixUIExtension`,
280 `-ext`,
281 `WixUtilExtension`,
282 `-cultures:en-us`,
283 `-o`,
284 msiOutputPath,
285 path.join(wixobjDir, "TypeAgent-AgentServer.wixobj"),
286 path.join(wixobjDir, "AgentServerFiles.wixobj"),
287 path.join(wixobjDir, "CopilotPluginFiles.wixobj"),
288]);
289
290if (!fs.existsSync(msiOutputPath)) {
291 console.error(`❌ MSI build failed: output file not created`);
292 process.exit(1);
293}
294
295const sizeMb = (fs.statSync(msiOutputPath).size / 1024 / 1024).toFixed(1);
296console.log(`\n✅ MSI build complete!`);
297console.log(` Output: ${msiOutputPath} (${sizeMb} MB)`);
298console.log(` Sign: node sign-msi.mjs "${msiOutputPath}"`);
299
300process.exit(0);
301