-
Notifications
You must be signed in to change notification settings - Fork 246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add support for bin-scripts (python only) #1941
Conversation
packages/@jsii/spec/lib/assembly.ts
Outdated
* | ||
* @default none | ||
*/ | ||
bin?: { [script: string]: string }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's tighten this one up from the start (cannot fix the older fields to readonly
because... backwards-compatibility... but we can be nice with the new ones!)
bin?: { [script: string]: string }; | |
bin?: { readonly [script: string]: string }; |
@@ -1429,6 +1429,13 @@ class PythonModule implements PythonType { | |||
code.line(`from ${'.'.repeat(distanceFromRoot + 1)}_jsii import *`); | |||
|
|||
this.emitRequiredImports(code, context); | |||
if (this.assembly.bin) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like being explicit here (JS' truthiness/falsiness is a little awkward)
if (this.assembly.bin) { | |
if (this.assembly.bin != null) { |
const scripts = Object.keys(this.assembly.bin); | ||
for (const script of scripts) { | ||
code.line(`print('${script}: ${this.assembly.bin[script]}');`); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const scripts = Object.keys(this.assembly.bin); | |
for (const script of scripts) { | |
code.line(`print('${script}: ${this.assembly.bin[script]}');`); | |
} | |
const scripts = Object.keys(this.assembly.bin); | |
for (const [name, path] of Object.entries(this.assembly.bin)) { | |
code.line(`print('${name}: ${path}');`); | |
} |
10723f3
to
c49762b
Compare
c49762b
to
aaf8d7e
Compare
1d7b7dc
to
628ac54
Compare
628ac54
to
7e9c9e2
Compare
af60019
to
b0c6f98
Compare
@@ -103,13 +103,13 @@ export interface LoadResponse { | |||
export interface InvokeScriptRequest { | |||
readonly pkgname: string; | |||
readonly script: string; | |||
readonly args?: any[]; | |||
readonly args?: string[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
readonly args?: string[]; | |
readonly args?: readonly string[]; |
packages/@jsii/kernel/lib/kernel.ts
Outdated
'node', | ||
[path.join(this._getPackageDir(req.pkgname), req.script)].concat( | ||
req.args ?? [], | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably going to be more efficient to use the same node
runtime that is used by the Kernel process (and it's also going to be safer in Windows):
process.execPath
contains the path to thenode
binary that runs the current process.process.execArgv
contains thenode
arguments that the current process received.
The --
indicates the end of node
options and guarantees that none of your own command options are possibly interpreted by node itself.
'node', | |
[path.join(this._getPackageDir(req.pkgname), req.script)].concat( | |
req.args ?? [], | |
), | |
process.execPath, | |
[ | |
...process.execArgv, | |
'--', | |
path.join(this._getPackageDir(req.pkgname), req.script), | |
...req.args ?? [], | |
], |
sandbox.create({ fqn: 'jsii-calc.Calculator' }); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessary
sandbox.create({ fqn: 'jsii-calc.Calculator' }); |
packages/@jsii/kernel/lib/api.ts
Outdated
@@ -100,6 +100,19 @@ export interface LoadResponse { | |||
readonly types: number; | |||
} | |||
|
|||
export interface InvokeScriptRequest { | |||
readonly pkgname: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather try to make sure we are consistent with how we refer to those concepts... in this case, we refer to assemblies (since the value that ought to be passed here is the same that was passed to LoadRequest.assembly
).
readonly pkgname: string; | |
readonly assembly: string; |
packages/@jsii/kernel/lib/kernel.ts
Outdated
): api.InvokeScriptResponse { | ||
const result = cp.spawnSync( | ||
'node', | ||
[path.join(this._getPackageDir(req.pkgname), req.script)].concat( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the script
value provided should not be a path, instead it should be a key from the bin
map that was gathered from package.json
during compilation.
You need to validate the script name was in the map, and fail if not (you don't want to allow a request to run anything from anywhere, that's kind of a bad security posture to have. Then, you need to use the value that corresponds to the bin
key that was requested in order to compose the final executable path.
Maybe we shouldn't actually use node
shell out here... Your NPM package's bin
script might not actually be a node script (could be shell script, could be something else). On the other hand it might be tricky to make this work smooth on Windows if we skip the shell out (I guess it's a good first step?)
packages/@jsii/kernel/lib/kernel.ts
Outdated
output: result.output, | ||
stdout: result.stdout, | ||
stderr: result.stderr, | ||
status: result.status, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be null
if the app was killed by a signal... Might want to also pass signal
though.
The contract for spawnSync
guarantees that exactly one of result.status
and result.signal
will be non-null
.
Following my suggestion for the type change above... This would be something like:
status: result.status, | |
status: result.status != null | |
? { code: result.status } | |
: { signal: result.signal! }, // <- The `!` is to signal we "know" `result.signal` cannot be `null` here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think that it is a good idea to add as result one of property. It could be really inconvenient to parse such property.
What do you think if we add signal as additional property?
packages/jsii/lib/assembler.ts
Outdated
fingerprint: '<TBD>', | ||
bin: this.projectInfo.bin, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's try to keep fingerprint
last here, at least for now.
fingerprint: '<TBD>', | |
bin: this.projectInfo.bin, | |
bin: this.projectInfo.bin, | |
fingerprint: '<TBD>', |
if (scripts.indexOf(script) < 0) { | ||
scripts.push(script); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the duplication defense here is not necessary (it also does not hurt!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not hurt but looks strange when the same script appeared several times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But... it shouldn't... I mean a script can only be registered once... so if we have duplicates, it could mean something wrong happened somewhere else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. I have made some tests and have not found any dublicates.
@@ -1631,6 +1681,8 @@ class Package { | |||
a.pythonName.localeCompare(b.pythonName), | |||
); | |||
|
|||
const scripts: string[] = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We tend to prefer this form for some reason:
const scripts: string[] = []; | |
const scripts = new Array<string>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just to try keep style of this file. Such structure appear 5 times in this script.
'__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)}`, | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You will probably run into issues doing that, because this flow never loads the current package's dependencies into the kernel...
Instead of re-doing this, you should import the module that actually initializes things correctly:
import ._jsii # <- And voilà, dependencies and the current module are loaded!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not really understand what do you mean here?
b0c6f98
to
ed032d9
Compare
ed032d9
to
dcfe863
Compare
So sorry - this was closed in error as the |
packages/@jsii/kernel/lib/kernel.ts
Outdated
const result = cp.spawnSync( | ||
process.execPath, | ||
[ | ||
...process.execArgv, | ||
'--', | ||
path.join(packageDir, scriptPath), | ||
...(req.args ?? []), | ||
], | ||
{ encoding: 'utf-8' }, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The script might not be a node script (could be bash, etc...), so this feels a little unsafe.
const result = cp.spawnSync( | |
process.execPath, | |
[ | |
...process.execArgv, | |
'--', | |
path.join(packageDir, scriptPath), | |
...(req.args ?? []), | |
], | |
{ encoding: 'utf-8' }, | |
); | |
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, | |
}, | |
); |
script: 'calc', | ||
}); | ||
|
||
expect(result.stdout).toEqual('Hello World!\n'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably should also asset the value of stderr
, status
and signal
.
@@ -85,6 +85,7 @@ | |||
from scope.jsii_calc_lib import IFriendly, EnumFromScopedModule, Number | |||
from scope.jsii_calc_lib.custom_submodule_name import IReflectable, ReflectableEntry | |||
|
|||
from subprocess import Popen, PIPE, STDOUT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't appear to be necessary.
scripts.push(script); | ||
} | ||
} | ||
Array.prototype.push.apply(scripts, mod.emitBinScripts(code)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not scripts.push(...mod.emitBinScripts(code))
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just not very familiar with typescript so had no idea about '...' operator. Your version is better.
@@ -7,6 +7,9 @@ | |||
], | |||
"url": "https://aws.amazon.com" | |||
}, | |||
"bin": { | |||
"calc": "bin/calc" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be interesting to rename bin/calc
to something else to remove the "coincidence" that it matches the script's name (the key here).
For example, you could call it bin/run
instead.
There's also a chance this does not test fine on Windows (because Windows is hard 🤓). In such cases you'd need to drop in a CMD entry point (I'll give you more info on this once we know this is necessary, hopefully it won't be).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
@Mergifyio update |
Command
|
Thank you for contributing! ❤️ I will now look into making sure the PR is up-to-date, then proceed to try and merge it! |
AWS CodeBuild CI Report
Powered by github-codebuild-logs, available on the AWS Serverless Application Repository |
Merging (with squash)... |
Problem
JSII is missing functionality for packing and using bin scripts from Python package.
Solution
Were extended assembly and project-info properties for bin-scripts.
Added generation scripts for each script in bin section.
Added section
scripts
topyhon.py
script.Testing
Was build project with changed scripts and checked that file
.jsii
containsbin
section.Also was checked that without
bin
section inpackage.json
emptybin
section was not created in.jsii
file.Was created package and verified that scripts were created and paths to it were added to
setup.py
.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.