packages/jsii-pacmak/lib/targets/go.ts (174 lines of code) (raw):
import { CodeMaker } from 'codemaker';
import * as fs from 'fs-extra';
import { Assembly } from 'jsii-reflect';
import { RosettaTabletReader } from 'jsii-rosetta';
import * as path from 'path';
import { IGenerator, Legalese } from '../generator';
import * as logging from '../logging';
import { findLocalBuildDirs, Target, TargetOptions } from '../target';
import { shell } from '../util';
import { Documentation } from './go/documentation';
import { GOMOD_FILENAME, RootPackage } from './go/package';
import { JSII_INIT_PACKAGE } from './go/runtime';
import { tarballName } from './go/util';
export class Golang extends Target {
private readonly goGenerator: GoGenerator;
public constructor(options: TargetOptions) {
super(options);
this.goGenerator = new GoGenerator(options);
}
public get generator() {
return this.goGenerator;
}
/**
* Generates a publishable artifact in `outDir`.
*
* @param sourceDir the directory where the generated source is located.
* @param outDir the directory where the publishable artifact should be placed.
*/
public async build(sourceDir: string, outDir: string): Promise<void> {
// copy generated sources to the output directory
await this.copyFiles(sourceDir, outDir);
const pkgDir = path.join(outDir, this.goGenerator.rootPackage.packageName);
// write `local.go.mod` with "replace" directives for local modules
const localGoMod = await this.writeLocalGoMod(pkgDir);
try {
// run `go build` with local.go.mod, go 1.16+ requires that we download
// modules explicit so go.sum is updated. We'd want to use
// `go mod download`, but it does not add missing entries in the `go.sum`
// file while `go mod tidy` does.
await go('mod', ['tidy', '-modfile', localGoMod.path], {
cwd: pkgDir,
});
} catch (e) {
logging.info(
`[${pkgDir}] Content of ${localGoMod.path} file:\n${localGoMod.content}`,
);
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject(e);
}
if (process.env.JSII_BUILD_GO) {
// This step is taken to ensure that the generated code is compilable
await go('build', ['-modfile', localGoMod.path, './...'], {
cwd: pkgDir,
});
}
// delete local.go.mod and local.go.sum from the output directory so it doesn't get published
const localGoSum = `${path.basename(localGoMod.path, '.mod')}.sum`;
await fs.remove(path.join(pkgDir, localGoMod.path));
return fs.remove(path.join(pkgDir, localGoSum));
}
/**
* Creates a copy of the `go.mod` file called `local.go.mod` with added
* `replace` directives for local mono-repo dependencies. This is required in
* order to run `go fmt` and `go build`.
*
* @param pkgDir The directory which contains the generated go code
*/
private async writeLocalGoMod(pkgDir: string) {
const replace: Record<string, string> = {};
// find local deps by check if `<jsii.outdir>/go` exists for dependencies
// and also consider `outDir` in case pacmak is executed using `--outdir
// --recurse` (in which case all go code will be generated there).
const dirs = [
path.dirname(pkgDir),
...(await findLocalBuildDirs(this.packageDir, 'go')),
];
// try to resolve @jsii/go-runtime (only exists as a devDependency)
const localModules = tryFindLocalRuntime();
if (localModules != null) {
for (const [name, localPath] of Object.entries(localModules)) {
replace[name] = localPath;
}
}
// iterate (recursively) on all package dependencies and check if we have a
// local build directory for this module. if
// we do, add a "replace" directive to point to it instead of download from
// the network.
const visit = async (pkg: RootPackage) => {
for (const dep of pkg.packageDependencies) {
for (const baseDir of dirs) {
// eslint-disable-next-line no-await-in-loop
const moduleDir = await tryFindLocalModule(baseDir, dep);
if (moduleDir) {
replace[dep.goModuleName] = moduleDir;
// we found a replacement for this dep, we can stop searching
break;
}
}
// recurse to transitive deps ("replace" is only considered at the top level go.mod)
// eslint-disable-next-line no-await-in-loop
await visit(dep);
}
};
await visit(this.goGenerator.rootPackage);
// write `local.go.mod`
// read existing content
const goMod = path.join(pkgDir, GOMOD_FILENAME);
const lines = [await fs.readFile(goMod, 'utf-8'), '', '// Local packages:'];
for (const [from, to] of Object.entries(replace)) {
logging.info(`[${pkgDir}] Local replace: ${from} => ${to}`);
lines.push(`replace ${from} => ${to}`);
}
const localGoMod = `local.${GOMOD_FILENAME}`;
const content = lines.join('\n');
await fs.writeFile(path.join(pkgDir, localGoMod), content, {
encoding: 'utf-8',
});
return { path: localGoMod, content };
}
}
class GoGenerator implements IGenerator {
private assembly!: Assembly;
public rootPackage!: RootPackage;
private readonly code = new CodeMaker({
indentCharacter: '\t',
indentationLevel: 1,
});
private readonly documenter: Documentation;
private readonly rosetta: RosettaTabletReader;
private readonly runtimeTypeChecking: boolean;
public constructor(options: {
readonly rosetta: RosettaTabletReader;
readonly runtimeTypeChecking: boolean;
}) {
this.rosetta = options.rosetta;
this.documenter = new Documentation(this.code, this.rosetta);
this.runtimeTypeChecking = options.runtimeTypeChecking;
}
public async load(_: string, assembly: Assembly): Promise<void> {
this.assembly = assembly;
return Promise.resolve();
}
public async upToDate(_outDir: string) {
return Promise.resolve(false);
}
public generate(): void {
this.rootPackage = new RootPackage(this.assembly);
return this.rootPackage.emit({
code: this.code,
documenter: this.documenter,
runtimeTypeChecking: this.runtimeTypeChecking,
});
}
public async save(
outDir: string,
tarball: string,
{ license, notice }: Legalese,
): Promise<any> {
const output = path.join(outDir, this.rootPackage.packageName);
await this.code.save(output);
await fs.copyFile(
tarball,
path.join(output, JSII_INIT_PACKAGE, tarballName(this.assembly)),
);
if (license) {
await fs.writeFile(path.join(output, 'LICENSE'), license, {
encoding: 'utf8',
});
}
if (notice) {
await fs.writeFile(path.join(output, 'NOTICE'), notice, {
encoding: 'utf8',
});
}
}
}
/**
* Checks if `buildDir` includes a local go build version (with "replace"
* directives).
* @param baseDir the `dist/go` directory
* @returns `undefined` if not or the module directory otherwise.
*/
async function tryFindLocalModule(baseDir: string, pkg: RootPackage) {
const gomodPath = path.join(baseDir, pkg.packageName, GOMOD_FILENAME);
if (!(await fs.pathExists(gomodPath))) {
return undefined;
}
// read `go.mod` and check that it is for the correct module
const gomod = (await fs.readFile(gomodPath, 'utf-8')).split('\n');
const isExpectedModule = gomod.find(
(line) => line.trim() === `module ${pkg.goModuleName}`,
);
if (!isExpectedModule) {
return undefined;
}
return path.resolve(path.dirname(gomodPath));
}
/**
* Check if we are running from inside the jsii repository, and then we want to
* use the local runtime instead of download from a released version.
*
* This is a generator that procudes an entry for each local module that
* is identified under the local module path exposed by `@jsii/go-runtime` .
*/
function tryFindLocalRuntime():
| { readonly [name: string]: string }
| undefined {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports, import/no-extraneous-dependencies
const localRuntime = require('@jsii/go-runtime');
logging.debug(`Using @jsii/go-runtime from ${localRuntime.runtimePath}`);
return localRuntime.runtimeModules;
} catch {
return undefined;
}
}
/**
* Executes a go CLI command.
*
*
* @param command The `go` command to execute (e.g. `build`)
* @param args Additional args
* @param options Options
*/
async function go(command: string, args: string[], options: { cwd: string }) {
const { cwd } = options;
return shell('go', [command, ...args], {
cwd,
env: {
// disable the use of sumdb to reduce eventual consistency issues when new modules are published
GOSUMDB: 'off',
},
});
}