Skip to content

Commit f1cc33a

Browse files
committed
feat(dlx-binary): add downloadBinary and executeBinary helpers
Adds downloadBinary and executeBinary functions to mirror the downloadPackage/executePackage pattern from dlx-package. - downloadBinary: Download binary with caching (without execution) - executeBinary: Execute cached binary without re-downloading - Renamed internal downloadBinary to downloadBinaryFile to avoid conflict All functions are exported for public use.
1 parent 12de0ae commit f1cc33a

1 file changed

Lines changed: 99 additions & 2 deletions

File tree

src/dlx-binary.ts

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ async function isCacheValid(
8181
/**
8282
* Download a file from a URL with integrity checking and concurrent download protection.
8383
* Uses downloadWithLock to prevent multiple processes from downloading the same binary simultaneously.
84+
* Internal helper function for downloading binary files.
8485
*/
85-
async function downloadBinary(
86+
async function downloadBinaryFile(
8687
url: string,
8788
destPath: string,
8889
checksum?: string | undefined,
@@ -270,7 +271,7 @@ export async function dlxBinary(
270271
await fs.mkdir(cacheEntryDir, { recursive: true })
271272

272273
// Download the binary.
273-
computedChecksum = await downloadBinary(url, binaryPath, checksum)
274+
computedChecksum = await downloadBinaryFile(url, binaryPath, checksum)
274275
await writeMetadata(cacheEntryDir, url, computedChecksum || '')
275276
}
276277

@@ -313,6 +314,102 @@ export async function dlxBinary(
313314
}
314315
}
315316

317+
/**
318+
* Download a binary from a URL with caching (without execution).
319+
* Similar to downloadPackage from dlx-package.
320+
*
321+
* @returns Object containing the path to the cached binary and whether it was downloaded
322+
*/
323+
export async function downloadBinary(
324+
options: Omit<DlxBinaryOptions, 'spawnOptions'>,
325+
): Promise<{ binaryPath: string; downloaded: boolean }> {
326+
const {
327+
cacheTtl = /*@__INLINE__*/ require('#constants/time').DLX_BINARY_CACHE_TTL,
328+
checksum,
329+
force = false,
330+
name,
331+
url,
332+
} = { __proto__: null, ...options } as DlxBinaryOptions
333+
334+
// Generate cache paths similar to pnpm/npx structure.
335+
const cacheDir = getDlxCachePath()
336+
const binaryName = name || `binary-${process.platform}-${os.arch()}`
337+
// Create spec from URL and binary name for unique cache identity.
338+
const spec = `${url}:${binaryName}`
339+
const cacheKey = generateCacheKey(spec)
340+
const cacheEntryDir = path.join(cacheDir, cacheKey)
341+
const binaryPath = normalizePath(path.join(cacheEntryDir, binaryName))
342+
343+
let downloaded = false
344+
345+
// Check if we need to download.
346+
if (
347+
!force &&
348+
existsSync(cacheEntryDir) &&
349+
(await isCacheValid(cacheEntryDir, cacheTtl))
350+
) {
351+
// Binary is cached and valid.
352+
downloaded = false
353+
} else {
354+
// Ensure cache directory exists.
355+
await fs.mkdir(cacheEntryDir, { recursive: true })
356+
357+
// Download the binary.
358+
const computedChecksum = await downloadBinaryFile(url, binaryPath, checksum)
359+
await writeMetadata(cacheEntryDir, url, computedChecksum || '')
360+
downloaded = true
361+
}
362+
363+
return {
364+
binaryPath,
365+
downloaded,
366+
}
367+
}
368+
369+
/**
370+
* Execute a cached binary without re-downloading.
371+
* Similar to executePackage from dlx-package.
372+
* Binary must have been previously downloaded via downloadBinary or dlxBinary.
373+
*
374+
* @param binaryPath Path to the cached binary (from downloadBinary result)
375+
* @param args Arguments to pass to the binary
376+
* @param spawnOptions Spawn options for execution
377+
* @param spawnExtra Extra spawn configuration
378+
* @returns The spawn promise for the running process
379+
*/
380+
export function executeBinary(
381+
binaryPath: string,
382+
args: readonly string[] | string[],
383+
spawnOptions?: SpawnOptions | undefined,
384+
spawnExtra?: SpawnExtra | undefined,
385+
): ReturnType<typeof spawn> {
386+
// On Windows, script files (.bat, .cmd, .ps1) require shell: true because
387+
// they are not executable on their own and must be run through cmd.exe.
388+
// Note: .exe files are actual binaries and don't need shell mode.
389+
const needsShell = WIN32 && /\.(?:bat|cmd|ps1)$/i.test(binaryPath)
390+
391+
// Windows cmd.exe PATH resolution behavior:
392+
// When shell: true on Windows with .cmd/.bat/.ps1 files, spawn will automatically
393+
// strip the full path down to just the basename without extension. Windows cmd.exe
394+
// then searches for the binary in directories listed in PATH.
395+
//
396+
// Since our binaries are downloaded to a custom cache directory that's not in PATH,
397+
// we must prepend the cache directory to PATH so cmd.exe can locate the binary.
398+
const cacheEntryDir = path.dirname(binaryPath)
399+
const finalSpawnOptions = needsShell
400+
? {
401+
...spawnOptions,
402+
env: {
403+
...spawnOptions?.env,
404+
PATH: `${cacheEntryDir}${path.delimiter}${process.env['PATH'] || ''}`,
405+
},
406+
shell: true,
407+
}
408+
: spawnOptions
409+
410+
return spawn(binaryPath, args, finalSpawnOptions, spawnExtra)
411+
}
412+
316413
/**
317414
* Get the DLX binary cache directory path.
318415
* Returns normalized path for cross-platform compatibility.

0 commit comments

Comments
 (0)