Skip to content
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

Merged
merged 18 commits into from
Dec 10, 2020

Conversation

kozlove-aws
Copy link
Contributor

@kozlove-aws kozlove-aws commented Aug 25, 2020

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 to pyhon.py script.

Testing

Was build project with changed scripts and checked that file .jsii contains bin section.
Also was checked that without bin section in package.json empty bin 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.

*
* @default none
*/
bin?: { [script: string]: string };
Copy link
Contributor

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!)

Suggested change
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) {
Copy link
Contributor

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)

Suggested change
if (this.assembly.bin) {
if (this.assembly.bin != null) {

Comment on lines 1434 to 1437
const scripts = Object.keys(this.assembly.bin);
for (const script of scripts) {
code.line(`print('${script}: ${this.assembly.bin[script]}');`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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}');`);
}

@RomainMuller RomainMuller changed the title feat: Add bin script folder feat: add support for bin-scripts (python only) Aug 25, 2020
@kozlove-aws kozlove-aws force-pushed the add_bin_script_folder branch from 10723f3 to c49762b Compare August 25, 2020 22:02
@kozlove-aws kozlove-aws force-pushed the add_bin_script_folder branch from c49762b to aaf8d7e Compare August 25, 2020 22:04
@kozlove-aws kozlove-aws force-pushed the add_bin_script_folder branch 3 times, most recently from 1d7b7dc to 628ac54 Compare September 10, 2020 23:56
@kozlove-aws kozlove-aws force-pushed the add_bin_script_folder branch 8 times, most recently from af60019 to b0c6f98 Compare October 6, 2020 01:27
@@ -103,13 +103,13 @@ export interface LoadResponse {
export interface InvokeScriptRequest {
readonly pkgname: string;
readonly script: string;
readonly args?: any[];
readonly args?: string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
readonly args?: string[];
readonly args?: readonly string[];

Comment on lines 143 to 146
'node',
[path.join(this._getPackageDir(req.pkgname), req.script)].concat(
req.args ?? [],
),
Copy link
Contributor

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 the node binary that runs the current process.
  • process.execArgv contains the node 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.

Suggested change
'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 ?? [],
],

Comment on lines 2137 to 2138
sandbox.create({ fqn: 'jsii-calc.Calculator' });

Copy link
Contributor

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

Suggested change
sandbox.create({ fqn: 'jsii-calc.Calculator' });

@@ -100,6 +100,19 @@ export interface LoadResponse {
readonly types: number;
}

export interface InvokeScriptRequest {
readonly pkgname: string;
Copy link
Contributor

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).

Suggested change
readonly pkgname: string;
readonly assembly: string;

): api.InvokeScriptResponse {
const result = cp.spawnSync(
'node',
[path.join(this._getPackageDir(req.pkgname), req.script)].concat(
Copy link
Contributor

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?)

output: result.output,
stdout: result.stdout,
stderr: result.stderr,
status: result.status,
Copy link
Contributor

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:

Suggested change
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

Copy link
Contributor Author

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?

fingerprint: '<TBD>',
bin: this.projectInfo.bin,
Copy link
Contributor

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.

Suggested change
fingerprint: '<TBD>',
bin: this.projectInfo.bin,
bin: this.projectInfo.bin,
fingerprint: '<TBD>',

Comment on lines 1699 to 1701
if (scripts.indexOf(script) < 0) {
scripts.push(script);
}
Copy link
Contributor

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!)

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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[] = [];
Copy link
Contributor

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:

Suggested change
const scripts: string[] = [];
const scripts = new Array<string>();

Copy link
Contributor Author

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.

Comment on lines +1503 to +1509
'__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)}`,
],
Copy link
Contributor

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!

Copy link
Contributor Author

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?

@kozlove-aws kozlove-aws force-pushed the add_bin_script_folder branch from b0c6f98 to ed032d9 Compare October 9, 2020 02:13
@kozlove-aws kozlove-aws force-pushed the add_bin_script_folder branch from ed032d9 to dcfe863 Compare October 14, 2020 16:17
@RomainMuller RomainMuller deleted the branch aws:main October 28, 2020 16:31
@RomainMuller RomainMuller reopened this Nov 5, 2020
@RomainMuller RomainMuller changed the base branch from master to main November 5, 2020 08:58
@RomainMuller
Copy link
Contributor

So sorry - this was closed in error as the master branch was renamed to main. Apologies!

Comment on lines 157 to 166
const result = cp.spawnSync(
process.execPath,
[
...process.execArgv,
'--',
path.join(packageDir, scriptPath),
...(req.args ?? []),
],
{ encoding: 'utf-8' },
);
Copy link
Contributor

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.

Suggested change
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');
Copy link
Contributor

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
Copy link
Contributor

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));
Copy link
Contributor

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))?

Copy link
Contributor Author

@kozlove-aws kozlove-aws Nov 6, 2020

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"
Copy link
Contributor

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).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

@kozlove-aws kozlove-aws marked this pull request as ready for review December 9, 2020 16:34
@RomainMuller
Copy link
Contributor

@Mergifyio update

@mergify
Copy link
Contributor

mergify bot commented Dec 10, 2020

Command update: success

Branch has been successfully updated

@mergify
Copy link
Contributor

mergify bot commented Dec 10, 2020

Thank you for contributing! ❤️ I will now look into making sure the PR is up-to-date, then proceed to try and merge it!

@mergify mergify bot added the pr/ready-to-merge This PR is ready to be merged. label Dec 10, 2020
@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-5lHf64IXfvmr
  • Commit ID: fc2ec7c
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@mergify mergify bot merged commit 61ef5ed into aws:main Dec 10, 2020
@mergify
Copy link
Contributor

mergify bot commented Dec 10, 2020

Merging (with squash)...

@mergify mergify bot removed the pr/ready-to-merge This PR is ready to be merged. label Dec 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants