diff --git a/packages/@jsii/kernel/lib/api.ts b/packages/@jsii/kernel/lib/api.ts index 1eb4f7b416..113077df57 100644 --- a/packages/@jsii/kernel/lib/api.ts +++ b/packages/@jsii/kernel/lib/api.ts @@ -100,6 +100,19 @@ export interface LoadResponse { readonly types: number; } +export interface InvokeScriptRequest { + readonly assembly: string; + readonly script: string; + readonly args?: string[]; +} + +export interface InvokeScriptResponse { + readonly status: number | null; + readonly stdout: Buffer; + readonly stderr: Buffer; + readonly signal: string | null; +} + export interface CreateRequest { /** * The FQN of the class of which an instance is requested (or "Object") diff --git a/packages/@jsii/kernel/lib/kernel.ts b/packages/@jsii/kernel/lib/kernel.ts index 7f112bfd9a..9d5caf0070 100644 --- a/packages/@jsii/kernel/lib/kernel.ts +++ b/packages/@jsii/kernel/lib/kernel.ts @@ -1,4 +1,5 @@ import * as spec from '@jsii/spec'; +import * as cp from 'child_process'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; @@ -65,24 +66,11 @@ export class Kernel { ); } - if (!this.installDir) { - this.installDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsii-kernel-')); - fs.mkdirpSync(path.join(this.installDir, 'node_modules')); - this._debug('creating jsii-kernel modules workdir:', this.installDir); - - process.on('exit', () => { - if (this.installDir) { - this._debug('removing install dir', this.installDir); - fs.removeSync(this.installDir); // can't use async version during exit - } - }); - } - const pkgname = req.name; const pkgver = req.version; // check if we already have such a module - const packageDir = path.join(this.installDir, 'node_modules', pkgname); + const packageDir = this._getPackageDir(pkgname); if (fs.pathExistsSync(packageDir)) { // module exists, verify version const epkg = fs.readJsonSync(path.join(packageDir, 'package.json')); @@ -147,6 +135,50 @@ export class Kernel { }; } + public invokeBinScript( + req: api.InvokeScriptRequest, + ): api.InvokeScriptResponse { + const packageDir = this._getPackageDir(req.assembly); + if (fs.pathExistsSync(packageDir)) { + // module exists, verify version + const epkg = fs.readJsonSync(path.join(packageDir, 'package.json')); + + if (!epkg.bin) { + throw new Error('There is no bin scripts defined for this package.'); + } + + const scriptPath = epkg.bin[req.script]; + + if (!epkg.bin) { + throw new Error(`Script with name ${req.script} was not defined.`); + } + + const result = cp.spawnSync( + path.join(packageDir, scriptPath), + req.args ?? [], + { + encoding: 'utf-8', + env: { + ...process.env, + // Make sure the current NODE_OPTIONS are honored if we shell out to node + NODE_OPTIONS: process.execArgv.join(' '), + // Make sure "this" node is ahead of $PATH just in case + PATH: `${path.dirname(process.execPath)}:${process.env.PATH}`, + }, + shell: true, + }, + ); + + return { + stdout: result.stdout, + stderr: result.stderr, + status: result.status, + signal: result.signal, + }; + } + throw new Error(`Package with name ${req.assembly} was not loaded.`); + } + public create(req: api.CreateRequest): api.CreateResponse { return this._create(req); } @@ -493,6 +525,22 @@ export class Kernel { } } + private _getPackageDir(pkgname: string): string { + if (!this.installDir) { + this.installDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsii-kernel-')); + fs.mkdirpSync(path.join(this.installDir, 'node_modules')); + this._debug('creating jsii-kernel modules workdir:', this.installDir); + + process.on('exit', () => { + if (this.installDir) { + this._debug('removing install dir', this.installDir); + fs.removeSync(this.installDir); // can't use async version during exit + } + }); + } + return path.join(this.installDir, 'node_modules', pkgname); + } + // prefixed with _ to allow calling this method internally without // getting it recorded for testing. private _create(req: api.CreateRequest): api.CreateResponse { diff --git a/packages/@jsii/kernel/test/kernel.test.ts b/packages/@jsii/kernel/test/kernel.test.ts index 16f903f35d..a0d0f956b7 100644 --- a/packages/@jsii/kernel/test/kernel.test.ts +++ b/packages/@jsii/kernel/test/kernel.test.ts @@ -2134,6 +2134,18 @@ defineTest('Override transitive property', (sandbox) => { expect(propValue).toBe('N3W'); }); +defineTest('invokeBinScript() return output', (sandbox) => { + const result = sandbox.invokeBinScript({ + assembly: 'jsii-calc', + script: 'calc', + }); + + expect(result.stdout).toEqual('Hello World!\n'); + expect(result.stderr).toEqual(''); + expect(result.status).toEqual(0); + expect(result.signal).toBeNull(); +}); + // ================================================================================================= const testNames: { [name: string]: boolean } = {}; @@ -2204,7 +2216,7 @@ async function preparePackage(module: string, useCache = true) { }); const stdout = new Array(); child.stdout.on('data', (chunk) => stdout.push(Buffer.from(chunk))); - child.once('exit', (code, signal) => { + child.once('close', (code, signal) => { if (code === 0) { return ok(); } diff --git a/packages/@jsii/kernel/test/recording.ts b/packages/@jsii/kernel/test/recording.ts index 2fe16911ec..0dd1f995c0 100644 --- a/packages/@jsii/kernel/test/recording.ts +++ b/packages/@jsii/kernel/test/recording.ts @@ -31,12 +31,11 @@ export function recordInteraction(kernel: Kernel, inputOutputLogPath: string) { const logfile = fs.createWriteStream(inputOutputLogPath); (kernel as any).logfile = logfile; - Object.getOwnPropertyNames(Kernel.prototype) - .filter((p) => !p.startsWith('_')) - .forEach((api) => { - const old = Object.getOwnPropertyDescriptor(Kernel.prototype, api)!; - + Object.entries(Object.getOwnPropertyDescriptors(Kernel.prototype)) + .filter(([p, v]) => !p.startsWith('_') && typeof v.value === 'function') + .forEach(([api, old]) => { Object.defineProperty(kernel, api, { + ...old, value(...args: any[]) { logInput({ api, ...args[0] }); try { @@ -68,12 +67,10 @@ export function recordInteraction(kernel: Kernel, inputOutputLogPath: string) { }); function logInput(obj: any) { - const inputLine = `${JSON.stringify(obj)}\n`; - logfile.write(`> ${inputLine}`); + logfile.write(`> ${JSON.stringify(obj)}\n`); } function logOutput(obj: any) { - const outputLine = `${JSON.stringify(obj)}\n`; - logfile.write(`< ${outputLine}`); + logfile.write(`< ${JSON.stringify(obj)}\n`); } } diff --git a/packages/@jsii/python-runtime/src/jsii/_kernel/__init__.py b/packages/@jsii/python-runtime/src/jsii/_kernel/__init__.py index 984f180bc8..ebf137cf48 100644 --- a/packages/@jsii/python-runtime/src/jsii/_kernel/__init__.py +++ b/packages/@jsii/python-runtime/src/jsii/_kernel/__init__.py @@ -33,6 +33,7 @@ GetResponse, InvokeRequest, InvokeResponse, + InvokeScriptRequest, KernelResponse, LoadRequest, ObjRef, @@ -242,6 +243,20 @@ def __init__(self, provider_class: Type[BaseProvider] = ProcessProvider) -> None def load(self, name: str, version: str, tarball: str) -> None: self.provider.load(LoadRequest(name=name, version=version, tarball=tarball)) + def invokeBinScript( + self, pkgname: str, script: str, args: Optional[List[Any]] = None + ) -> None: + if args is None: + args = [] + + self.provider.invokeBinScript( + InvokeScriptRequest( + pkgname=pkgname, + script=script, + args=_make_reference_for_native(self, args), + ) + ) + # TODO: Is there a way to say that obj has to be an instance of klass? def create(self, klass: Type, obj: Any, args: Optional[List[Any]] = None) -> ObjRef: if args is None: diff --git a/packages/@jsii/python-runtime/src/jsii/_kernel/providers/base.py b/packages/@jsii/python-runtime/src/jsii/_kernel/providers/base.py index 145c654804..06ba5347e5 100644 --- a/packages/@jsii/python-runtime/src/jsii/_kernel/providers/base.py +++ b/packages/@jsii/python-runtime/src/jsii/_kernel/providers/base.py @@ -11,6 +11,8 @@ GetResponse, InvokeRequest, InvokeResponse, + InvokeScriptRequest, + InvokeScriptResponse, DeleteRequest, DeleteResponse, SetRequest, @@ -45,6 +47,10 @@ class BaseProvider(metaclass=abc.ABCMeta): def load(self, request: LoadRequest) -> LoadResponse: ... + @abc.abstractmethod + def invokeBinScript(self, request: InvokeScriptRequest) -> InvokeScriptResponse: + ... + @abc.abstractmethod def create(self, request: CreateRequest) -> CreateResponse: ... diff --git a/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py b/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py index 1fb6fac3ee..ac178785cd 100644 --- a/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py +++ b/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py @@ -38,6 +38,8 @@ GetResponse, InvokeRequest, InvokeResponse, + InvokeScriptRequest, + InvokeScriptResponse, SetRequest, SetResponse, StaticGetRequest, @@ -158,6 +160,10 @@ def __init__(self): LoadRequest, _with_api_key("load", self._serializer.unstructure_attrs_asdict), ) + self._serializer.register_unstructure_hook( + InvokeScriptRequest, + _with_api_key("invokeBinScript", self._serializer.unstructure_attrs_asdict), + ) self._serializer.register_unstructure_hook( CreateRequest, _with_api_key("create", self._serializer.unstructure_attrs_asdict), @@ -343,6 +349,9 @@ def _process(self) -> _NodeProcess: def load(self, request: LoadRequest) -> LoadResponse: return self._process.send(request, LoadResponse) + def invokeBinScript(self, request: InvokeScriptRequest) -> InvokeScriptResponse: + return self._process.send(request, InvokeScriptResponse) + def create(self, request: CreateRequest) -> CreateResponse: return self._process.send(request, CreateResponse) diff --git a/packages/@jsii/python-runtime/src/jsii/_kernel/types.py b/packages/@jsii/python-runtime/src/jsii/_kernel/types.py index 8adf783748..cc0f73c022 100644 --- a/packages/@jsii/python-runtime/src/jsii/_kernel/types.py +++ b/packages/@jsii/python-runtime/src/jsii/_kernel/types.py @@ -47,6 +47,23 @@ class LoadResponse: types: int +@attr.s(auto_attribs=True, frozen=True, slots=True) +class InvokeScriptRequest: + + pkgname: str + script: str + args: List[Any] = attr.Factory(list) + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class InvokeScriptResponse: + + status: int + stdout: str + stderr: str + output: List[str] + + @attr.s(auto_attribs=True, frozen=True, slots=True) class CreateRequest: @@ -226,6 +243,7 @@ class StatsResponse: GetRequest, StaticGetRequest, InvokeRequest, + InvokeScriptRequest, StaticInvokeRequest, StatsRequest, ] @@ -237,6 +255,7 @@ class StatsResponse: DeleteResponse, GetResponse, InvokeResponse, + InvokeScriptResponse, SetResponse, StatsResponse, Callback, diff --git a/packages/@jsii/python-runtime/src/jsii/_runtime.py b/packages/@jsii/python-runtime/src/jsii/_runtime.py index c632035556..b756c72cf9 100644 --- a/packages/@jsii/python-runtime/src/jsii/_runtime.py +++ b/packages/@jsii/python-runtime/src/jsii/_runtime.py @@ -45,6 +45,10 @@ def load(cls, *args, _kernel=kernel, **kwargs): # Give our record of the assembly back to the caller. return assembly + def invokeBinScript(cls, pkgname, script, *args, _kernel=kernel): + response = _kernel.invokeBinScript(pkgname, script, *args) + print(response.stdout) + class JSIIMeta(_ClassPropertyMeta, type): def __new__(cls, name, bases, attrs, *, jsii_type=None): diff --git a/packages/@jsii/python-runtime/tests/test_compliance.py b/packages/@jsii/python-runtime/tests/test_compliance.py index 9262679811..28aa6b6220 100644 --- a/packages/@jsii/python-runtime/tests/test_compliance.py +++ b/packages/@jsii/python-runtime/tests/test_compliance.py @@ -85,7 +85,6 @@ from scope.jsii_calc_lib import IFriendly, EnumFromScopedModule, Number from scope.jsii_calc_lib.custom_submodule_name import IReflectable, ReflectableEntry - # Note: The names of these test functions have been chosen to map as closely to the # Java Compliance tests as possible. # Note: While we could write more expressive and better tests using the functionality diff --git a/packages/@jsii/spec/lib/assembly.ts b/packages/@jsii/spec/lib/assembly.ts index 51663e47ad..1502da404a 100644 --- a/packages/@jsii/spec/lib/assembly.ts +++ b/packages/@jsii/spec/lib/assembly.ts @@ -143,6 +143,13 @@ export interface Assembly extends AssemblyConfiguration, Documentable { * @default none */ readme?: { markdown: string }; + + /** + * List of bin-scripts + * + * @default none + */ + bin?: { readonly [script: string]: string }; } /** diff --git a/packages/jsii-calc/bin/run b/packages/jsii-calc/bin/run new file mode 100755 index 0000000000..ea7dde785e --- /dev/null +++ b/packages/jsii-calc/bin/run @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./run.js'); diff --git a/packages/jsii-calc/bin/run.cmd b/packages/jsii-calc/bin/run.cmd new file mode 100644 index 0000000000..8c083b0bff --- /dev/null +++ b/packages/jsii-calc/bin/run.cmd @@ -0,0 +1,2 @@ +@echo off +node "%~dp0\run" %* diff --git a/packages/jsii-calc/bin/run.ts b/packages/jsii-calc/bin/run.ts new file mode 100755 index 0000000000..296a77b93d --- /dev/null +++ b/packages/jsii-calc/bin/run.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +/* eslint-disable no-console */ + +console.info('Hello World!'); diff --git a/packages/jsii-calc/package.json b/packages/jsii-calc/package.json index 6bcf20ec04..7fee842f59 100644 --- a/packages/jsii-calc/package.json +++ b/packages/jsii-calc/package.json @@ -14,6 +14,9 @@ "bugs": { "url": "https://github.com/aws/jsii/issues" }, + "bin": { + "calc": "bin/run" + }, "repository": { "type": "git", "url": "https://github.com/aws/jsii.git", diff --git a/packages/jsii-calc/test/assembly.jsii b/packages/jsii-calc/test/assembly.jsii index 4e4cdd3388..03e458ddc0 100644 --- a/packages/jsii-calc/test/assembly.jsii +++ b/packages/jsii-calc/test/assembly.jsii @@ -7,6 +7,9 @@ ], "url": "https://aws.amazon.com" }, + "bin": { + "calc": "bin/run" + }, "bundled": { "@fixtures/jsii-calc-bundled": "^0.19.0" }, @@ -14342,5 +14345,5 @@ } }, "version": "0.0.0", - "fingerprint": "55ZmA4GbUYPUmTXM2oFDEND8/Yk2Vzw1FThRWEOigAM=" + "fingerprint": "XfCnzPEGbEJR6hhBX8zWyFzWJP2wH9ztW2ZcW6Wdb+4=" } diff --git a/packages/jsii-pacmak/lib/targets/python.ts b/packages/jsii-pacmak/lib/targets/python.ts index cb22d01db2..0e8c730e95 100644 --- a/packages/jsii-pacmak/lib/targets/python.ts +++ b/packages/jsii-pacmak/lib/targets/python.ts @@ -1482,6 +1482,56 @@ class PythonModule implements PythonType { code.line('publication.publish()'); } + /** + * Emit the bin scripts if bin section defined. + */ + public emitBinScripts(code: CodeMaker): string[] { + const scripts = new Array(); + if (this.loadAssembly) { + if (this.assembly.bin != null) { + for (const name of Object.keys(this.assembly.bin)) { + const script_file = path.join( + 'src', + pythonModuleNameToFilename(this.pythonName), + 'bin', + name, + ); + code.openFile(script_file); + code.line('#!/usr/bin/env python'); + code.line(); + code.line('import jsii'); + code.line('import sys'); + code.line(); + emitList( + code, + '__jsii_assembly__ = jsii.JSIIAssembly.load(', + [ + JSON.stringify(this.assembly.name), + JSON.stringify(this.assembly.version), + JSON.stringify(this.pythonName.replace('._jsii', '')), + `${JSON.stringify(this.assemblyFilename)}`, + ], + ')', + ); + code.line(); + emitList( + code, + '__jsii_assembly__.invokeBinScript(', + [ + JSON.stringify(this.assembly.name), + JSON.stringify(name), + 'sys.argv[1:]', + ], + ')', + ); + code.closeFile(script_file); + scripts.push(script_file.replace(/\\/g, '/')); + } + } + } + return scripts; + } + /** * Emit the README as module docstring if this is the entry point module (it loads the assembly) */ @@ -1635,6 +1685,8 @@ class Package { a.pythonName.localeCompare(b.pythonName), ); + const scripts = new Array(); + // Iterate over all of our modules, and write them out to disk. for (const mod of modules) { const filename = path.join( @@ -1646,6 +1698,8 @@ class Package { code.openFile(filename); mod.emit(code, context); code.closeFile(filename); + + scripts.push(...mod.emitBinScripts(code)); } // Handle our package data. @@ -1725,6 +1779,7 @@ class Package { 'Programming Language :: Python :: 3.9', 'Typing :: Typed', ], + scripts, }; switch (this.metadata.docs?.stability) { diff --git a/packages/jsii-pacmak/test/generated-code/__snapshots__/prerelease-identifiers.test.ts.snap b/packages/jsii-pacmak/test/generated-code/__snapshots__/prerelease-identifiers.test.ts.snap index 8feb75b5ba..137ece8458 100644 --- a/packages/jsii-pacmak/test/generated-code/__snapshots__/prerelease-identifiers.test.ts.snap +++ b/packages/jsii-pacmak/test/generated-code/__snapshots__/prerelease-identifiers.test.ts.snap @@ -457,7 +457,8 @@ kwargs = json.loads( "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed" - ] + ], + "scripts": [] } """ ) @@ -954,7 +955,8 @@ kwargs = json.loads( "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed" - ] + ], + "scripts": [] } """ ) @@ -1429,7 +1431,8 @@ kwargs = json.loads( "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed" - ] + ], + "scripts": [] } """ ) @@ -1902,7 +1905,8 @@ kwargs = json.loads( "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Typing :: Typed" - ] + ], + "scripts": [] } """ ) diff --git a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-python.test.ts.snap b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-python.test.ts.snap index 4917348311..bef174043f 100644 --- a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-python.test.ts.snap +++ b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-python.test.ts.snap @@ -86,7 +86,8 @@ kwargs = json.loads( "Programming Language :: Python :: 3.9", "Typing :: Typed", "License :: OSI Approved" - ] + ], + "scripts": [] } """ ) @@ -333,7 +334,8 @@ kwargs = json.loads( "Programming Language :: Python :: 3.9", "Typing :: Typed", "License :: OSI Approved" - ] + ], + "scripts": [] } """ ) @@ -576,7 +578,8 @@ kwargs = json.loads( "Typing :: Typed", "Development Status :: 7 - Inactive", "License :: OSI Approved" - ] + ], + "scripts": [] } """ ) @@ -1287,6 +1290,8 @@ exports[`Generated code for "jsii-calc": / 1`] = ` ┣━ 📄 __init__.py ┣━ 📁 _jsii ┃ ┣━ 📄 __init__.py + ┃ ┣━ 📁 bin + ┃ ┃ ┗━ 📄 calc ┃ ┗━ 📄 jsii-calc@0.0.0.jsii.tgz ┣━ 📁 composition ┃ ┗━ 📄 __init__.py @@ -1429,6 +1434,9 @@ kwargs = json.loads( "Development Status :: 5 - Production/Stable", "License :: OSI Approved", "Test :: Classifier :: Is Dummy" + ], + "scripts": [ + "src/jsii_calc/_jsii/bin/calc" ] } """ @@ -8748,6 +8756,20 @@ publication.publish() `; +exports[`Generated code for "jsii-calc": /python/src/jsii_calc/_jsii/bin/calc 1`] = ` +#!/usr/bin/env python + +import jsii +import sys + +__jsii_assembly__ = jsii.JSIIAssembly.load( + "jsii-calc", "0.0.0", "jsii_calc", "jsii-calc@0.0.0.jsii.tgz" +) + +__jsii_assembly__.invokeBinScript("jsii-calc", "calc", sys.argv[1:]) + +`; + exports[`Generated code for "jsii-calc": /python/src/jsii_calc/_jsii/jsii-calc@0.0.0.jsii.tgz 1`] = `python/src/jsii_calc/_jsii/jsii-calc@0.0.0.jsii.tgz is a tarball`; exports[`Generated code for "jsii-calc": /python/src/jsii_calc/composition/__init__.py 1`] = ` diff --git a/packages/jsii/lib/assembler.ts b/packages/jsii/lib/assembler.ts index e09d865ca7..f756983558 100644 --- a/packages/jsii/lib/assembler.ts +++ b/packages/jsii/lib/assembler.ts @@ -200,6 +200,7 @@ export class Assembler implements Emitter { docs, readme, jsiiVersion, + bin: this.projectInfo.bin, fingerprint: '', }; diff --git a/packages/jsii/lib/project-info.ts b/packages/jsii/lib/project-info.ts index 2d037a82ac..cb5ba2a1ce 100644 --- a/packages/jsii/lib/project-info.ts +++ b/packages/jsii/lib/project-info.ts @@ -52,6 +52,7 @@ export interface ProjectInfo { readonly excludeTypescript: string[]; readonly projectReferences?: boolean; readonly tsc?: TSCompilerOptions; + readonly bin?: { readonly [name: string]: string }; } export async function loadProjectInfo( @@ -217,6 +218,7 @@ export async function loadProjectInfo( outDir: pkg.jsii?.tsc?.outDir, rootDir: pkg.jsii?.tsc?.rootDir, }, + bin: pkg.bin, diagnostics: _loadDiagnostics(pkg.jsii?.diagnostics), }; }