Skip to content

Commit

Permalink
feat(options): add envDir and installDeps
Browse files Browse the repository at this point in the history
refactor: pass execa.Options instead of Context to most functions,
make tsconfig more strict

BREAKING CHANGE: create venv

The new default behaviour of this plugin is to create a virtual environment before installing the python dependencies
  • Loading branch information
abichinger committed Oct 20, 2024
1 parent b196416 commit 7c936a1
Show file tree
Hide file tree
Showing 13 changed files with 451 additions and 277 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ Working examples using Github Actions can be found here:
| ```pypiPublish``` | bool | ```true``` | Whether to publish the python package to the pypi registry. If false the package version will still be updated.
| ```gpgSign``` | bool | ```false``` | Whether to sign the package using GPG. A valid PGP key must already be installed and configured on the host.
| ```gpgIdentity``` | str | ```null``` | When ```gpgSign``` is true, set the GPG identify to use when signing files. Leave empty to use the default identity.
| ```envDir``` | string \| ```false``` | ```.venv``` | directory to create the virtual environment in, if set to `false` no environment will be created
| ```installDeps``` | bool | ```true``` | wether to automatically install python dependencies

## Development

Expand Down
10 changes: 9 additions & 1 deletion lib/default-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ export class DefaultConfig {
}

public get gpgIdentity() {
return this.config.gpgIdentity ?? null;
return this.config.gpgIdentity;
}

public get envDir() {
return this.config.envDir ?? '.venv';
}

public get installDeps() {
return this.config.installDeps ?? true;
}
}
113 changes: 62 additions & 51 deletions lib/prepare.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import execa from 'execa';
import { execa, Options } from 'execa';
import fs from 'fs';
import os from 'os';
import path from 'path';
import type { Context } from './@types/semantic-release';
import { DefaultConfig } from './default-options';
import { PluginConfig } from './types';
import { normalizeVersion, setopt } from './util';
import { normalizeVersion, pipe, setopt } from './util';
import { assertExitCode, isLegacyBuildInterface } from './verify';

async function setVersionPy(setupPy: string, version: string) {
Expand All @@ -18,103 +19,113 @@ async function setVersionPy(setupPy: string, version: string) {
async function setVersionToml(
srcDir: string,
version: string,
context?: Context,
options?: Options,
) {
await assertExitCode(
'python3',
[path.resolve(__dirname, 'py/set_version.py'), '-v', version, srcDir],
{},
options,
0,
context,
);
}

async function sDistPackage(
srcDir: string,
distDir: string,
context?: Context,
options?: Options,
) {
const cp = execa('python3', ['-m', 'build', '--sdist', '--outdir', distDir], {
await execa('python3', ['-m', 'build', '--sdist', '--outdir', distDir], {
...options,
cwd: srcDir,
});

if (context) {
cp.stdout?.pipe(context.stdout, { end: false });
cp.stderr?.pipe(context.stderr, { end: false });
}

await cp;
}

async function bDistPackage(
srcDir: string,
distDir: string,
context?: Context,
options?: Options,
) {
try {
const cp = execa(
'python3',
['-m', 'build', '--wheel', '--outdir', distDir],
{
cwd: srcDir,
},
);

if (context) {
cp.stdout?.pipe(context.stdout, { end: false });
cp.stderr?.pipe(context.stderr, { end: false });
}

await cp;
await execa('python3', ['-m', 'build', '--wheel', '--outdir', distDir], {
...options,
cwd: srcDir,
});
} catch (err) {
console.log(err);
throw Error(`failed to build wheel`);
}
}

async function installPackages(packages: string[], context?: Context) {
const cp = execa('pip3', ['install', ...packages]);
async function installPackages(packages: string[], options?: Options) {
await execa('pip3', ['install', ...packages], options);
}

if (context) {
cp.stdout?.pipe(context.stdout, { end: false });
cp.stderr?.pipe(context.stderr, { end: false });
async function createVenv(envDir: string, options?: Options): Promise<Options> {
await execa('python3', ['-m', 'venv', envDir], options);
const envPath = path.resolve(envDir, 'bin');
if (os.platform() == 'win32') {
return {
...options,
env: {
Path: envPath + ';' + process.env.Path,
},
};
}

await cp;
return {
...options,
env: {
PATH: envPath + ':' + process.env.PATH,
},
};
}

async function prepare(pluginConfig: PluginConfig, context: Context) {
const { logger, nextRelease } = context;
const { srcDir, setupPath, distDir } = new DefaultConfig(pluginConfig);
const { srcDir, setupPath, distDir, envDir, installDeps } = new DefaultConfig(
pluginConfig,
);

const requirementsFile = path.resolve(__dirname, 'py/requirements.txt');
const requirements = fs
.readFileSync(requirementsFile, 'utf8')
.split('\n')
.filter((value) => value.length >= 0);
if (nextRelease === undefined) {
throw new Error('nextRelease is undefined');
}

logger.log(
`Installing required python packages (${requirements.join(', ')})`,
);
await installPackages(requirements, context);
let execaOptions: Options = pipe(context);

if (envDir) {
logger.log(`Creating virtual environment ${envDir}`);
execaOptions = await createVenv(envDir, execaOptions);
}

if (installDeps) {
const requirementsFile = path.resolve(__dirname, 'py/requirements.txt');
const requirements = fs
.readFileSync(requirementsFile, 'utf8')
.split('\n')
.filter((value) => value.length >= 0);

logger.log(
`Installing required python packages (${requirements.join(', ')})`,
);
await installPackages(requirements, execaOptions);
}

const version = await normalizeVersion(nextRelease.version);
const version = await normalizeVersion(nextRelease.version, execaOptions);

if (isLegacyBuildInterface(srcDir)) {
logger.log(`Set version to ${version} (setup.cfg)`);
await setVersionPy(setupPath, version);
} else {
await setVersionToml(srcDir, version, context);
await setVersionToml(srcDir, version, execaOptions);
}

logger.log(`Build source archive`);
await sDistPackage(srcDir, distDir, context);
await sDistPackage(srcDir, distDir, execaOptions);
logger.log(`Build wheel`);
await bDistPackage(srcDir, distDir, context);
await bDistPackage(srcDir, distDir, execaOptions);
}

export {
bDistPackage,
createVenv,
installPackages,
prepare,
sDistPackage,
Expand Down
28 changes: 16 additions & 12 deletions lib/publish.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import execa from 'execa';
import { execa, Options, ResultPromise } from 'execa';
import type { Context } from './@types/semantic-release';
import { DefaultConfig } from './default-options';
import { PluginConfig } from './types';
import { pipe } from './util.js';

function publishPackage(
srcDir: string,
distDir: string,
repoUrl: string,
gpgSign: boolean,
gpgIdentity: string,
) {
gpgIdentity?: string,
options?: Options,
): ResultPromise {
const signArgs = gpgSign ? ['--sign'] : [];
if (gpgIdentity) {
signArgs.push('--identity', gpgIdentity);
}

return execa(
'python3',
[
Expand All @@ -21,14 +28,14 @@ function publishPackage(
'--non-interactive',
'--skip-existing',
'--verbose',
gpgSign ? '--sign' : null,
gpgSign && gpgIdentity ? '--identity' : null,
gpgSign && gpgIdentity ? gpgIdentity : null,
...signArgs,
`${distDir}/*`,
].filter((arg) => arg !== null),
{
...options,
cwd: srcDir,
env: {
...options?.env,
TWINE_USERNAME: process.env['PYPI_USERNAME']
? process.env['PYPI_USERNAME']
: '__token__',
Expand All @@ -38,10 +45,8 @@ function publishPackage(
);
}

async function publish(
pluginConfig: PluginConfig,
{ logger, stdout, stderr }: Context,
) {
async function publish(pluginConfig: PluginConfig, context: Context) {
const { logger } = context;
const { srcDir, distDir, pypiPublish, gpgSign, gpgIdentity, repoUrl } =
new DefaultConfig(pluginConfig);

Expand All @@ -53,9 +58,8 @@ async function publish(
process.env['PYPI_REPO_URL'] ?? repoUrl,
gpgSign,
gpgIdentity,
pipe(context),
);
result.stdout?.pipe(stdout, { end: false });
result.stderr?.pipe(stderr, { end: false });
await result;
} else {
logger.log('Not publishing package due to requested configuration');
Expand Down
2 changes: 2 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export interface PluginConfig {
pypiPublish?: boolean;
gpgSign?: boolean;
gpgIdentity?: string;
envDir?: string | false;
installDeps?: boolean;
}
29 changes: 22 additions & 7 deletions lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import execa from 'execa';
import { execa, Options } from 'execa';
import path from 'path';
import { Context } from './@types/semantic-release';

async function normalizeVersion(version: string) {
const { stdout } = await execa('python3', [
'-c',
`from packaging.version import Version\nprint(Version('${version}'))`,
]);
async function normalizeVersion(
version: string,
options: Options = {},
): Promise<string> {
const { stdout } = await execa(
'python3',
[
'-c',
`from packaging.version import Version\nprint(Version('${version}'))`,
],
options,
);
return stdout;
}

Expand All @@ -28,4 +36,11 @@ function setopt(
);
}

export { normalizeVersion, setopt };
function pipe(context: Context): Options {
return {
stdout: context.stdout,
stderr: context.stderr,
};
}

export { normalizeVersion, pipe, setopt };
Loading

0 comments on commit 7c936a1

Please sign in to comment.