Skip to content

Commit caf6b5a

Browse files
authored
fix: convert Windows .exe console scripts to Linux for cross-platform deployment (#36)
When packaging Python dependencies on Windows for Linux targets (AWS Lambda/AgentCore), uv generates .exe console scripts based on the host OS rather than the target platform. This causes deployment failures with errors like 'OpenTelemetry instrumentation executable not found'. This fix adds post-processing to convert known Windows .exe scripts (opentelemetry-instrument, opentelemetry-bootstrap) to Linux-compatible shell scripts after uv pip install completes.
1 parent ce66e2d commit caf6b5a

File tree

2 files changed

+115
-1
lines changed

2 files changed

+115
-1
lines changed

src/lib/packaging/helpers.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import {
1515
readdirSync,
1616
rmSync,
1717
statSync,
18+
unlinkSync,
1819
writeFileSync,
1920
} from 'fs';
20-
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'fs/promises';
21+
import { mkdir, readFile, readdir, rm, stat, unlink, writeFile } from 'fs/promises';
2122
import { dirname, isAbsolute, join, parse, resolve } from 'path';
2223
import { pipeline } from 'stream/promises';
2324

@@ -349,3 +350,110 @@ export function enforceZipSizeLimitSync(zipPath: string): number {
349350
}
350351
return size;
351352
}
353+
354+
/**
355+
* Generates a Linux-compatible shell script for a Python console entry point.
356+
* This is needed when packaging on Windows for Linux targets, as uv generates
357+
* .exe files based on the host OS rather than the target platform.
358+
*/
359+
function generateLinuxConsoleScript(modulePath: string, funcName: string): string {
360+
return `#!/usr/bin/env python3
361+
# -*- coding: utf-8 -*-
362+
import re
363+
import sys
364+
from ${modulePath} import ${funcName}
365+
if __name__ == '__main__':
366+
sys.argv[0] = re.sub(r'(-script\\.pyw|\\.exe)?$', '', sys.argv[0])
367+
sys.exit(${funcName}())
368+
`;
369+
}
370+
371+
/**
372+
* Known console script entry points that need to be converted from Windows .exe
373+
* to Linux shell scripts when cross-compiling.
374+
* Format: { 'script-name': ['module.path', 'function_name'] }
375+
*/
376+
const KNOWN_CONSOLE_SCRIPTS: Record<string, [string, string]> = {
377+
'opentelemetry-instrument': ['opentelemetry.instrumentation.auto_instrumentation', 'run'],
378+
'opentelemetry-bootstrap': ['opentelemetry.instrumentation.bootstrap', 'run'],
379+
};
380+
381+
/**
382+
* Converts Windows .exe console scripts to Linux-compatible shell scripts.
383+
* This is necessary when packaging Python dependencies on Windows for deployment
384+
* to Linux-based AWS runtimes.
385+
*
386+
* @param stagingDir The directory containing installed Python packages
387+
*/
388+
export async function convertWindowsScriptsToLinux(stagingDir: string): Promise<void> {
389+
if (!isWindows) {
390+
return; // Only needed when building on Windows
391+
}
392+
393+
const binDir = join(stagingDir, 'bin');
394+
if (!(await pathExists(binDir))) {
395+
return;
396+
}
397+
398+
const entries = await readdir(binDir);
399+
400+
for (const entry of entries) {
401+
if (!entry.endsWith('.exe')) {
402+
continue;
403+
}
404+
405+
const scriptName = entry.slice(0, -4); // Remove .exe extension
406+
const entryPoint = KNOWN_CONSOLE_SCRIPTS[scriptName];
407+
408+
if (entryPoint) {
409+
const [modulePath, funcName] = entryPoint;
410+
const exePath = join(binDir, entry);
411+
const scriptPath = join(binDir, scriptName);
412+
413+
// Remove the Windows .exe file
414+
await unlink(exePath);
415+
416+
// Create Linux-compatible shell script
417+
const scriptContent = generateLinuxConsoleScript(modulePath, funcName);
418+
await writeFile(scriptPath, scriptContent, { mode: 0o755 });
419+
}
420+
}
421+
}
422+
423+
/**
424+
* Synchronous version of convertWindowsScriptsToLinux.
425+
*/
426+
export function convertWindowsScriptsToLinuxSync(stagingDir: string): void {
427+
if (!isWindows) {
428+
return; // Only needed when building on Windows
429+
}
430+
431+
const binDir = join(stagingDir, 'bin');
432+
if (!pathExistsSync(binDir)) {
433+
return;
434+
}
435+
436+
const entries = readdirSync(binDir);
437+
438+
for (const entry of entries) {
439+
if (!entry.endsWith('.exe')) {
440+
continue;
441+
}
442+
443+
const scriptName = entry.slice(0, -4); // Remove .exe extension
444+
const entryPoint = KNOWN_CONSOLE_SCRIPTS[scriptName];
445+
446+
if (entryPoint) {
447+
const [modulePath, funcName] = entryPoint;
448+
const exePath = join(binDir, entry);
449+
const scriptPath = join(binDir, scriptName);
450+
451+
// Remove the Windows .exe file
452+
unlinkSync(exePath);
453+
454+
// Create Linux-compatible shell script
455+
const scriptContent = generateLinuxConsoleScript(modulePath, funcName);
456+
writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
457+
}
458+
}
459+
}

src/lib/packaging/python.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { UV_INSTALL_HINT, getArtifactZipName } from '../constants';
33
import { runSubprocessCapture, runSubprocessCaptureSync } from '../utils/subprocess';
44
import { PackagingError } from './errors';
55
import {
6+
convertWindowsScriptsToLinux,
7+
convertWindowsScriptsToLinuxSync,
68
copySourceTree,
79
copySourceTreeSync,
810
createZipFromDir,
@@ -110,6 +112,8 @@ export class PythonCodeZipPackager implements RuntimePackager {
110112

111113
if (result.code === 0) {
112114
await copySourceTree(srcDir, stagingDir);
115+
// Convert Windows .exe scripts to Linux shell scripts for cross-platform deployment
116+
await convertWindowsScriptsToLinux(stagingDir);
113117
return stagingDir;
114118
} else {
115119
const platformIssue = detectUnavailablePlatform(result);
@@ -219,6 +223,8 @@ export class PythonCodeZipPackagerSync implements CodeZipPackager {
219223

220224
if (result.code === 0) {
221225
copySourceTreeSync(srcDir, stagingDir);
226+
// Convert Windows .exe scripts to Linux shell scripts for cross-platform deployment
227+
convertWindowsScriptsToLinuxSync(stagingDir);
222228
return stagingDir;
223229
} else {
224230
const platformIssue = detectUnavailablePlatform(result);

0 commit comments

Comments
 (0)