packages/jsii-pacmak/lib/targets/dotnet.ts (250 lines of code) (raw):

import * as spec from '@jsii/spec'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as xmlbuilder from 'xmlbuilder'; import { TargetBuilder, BuildOptions } from '../builder'; import * as logging from '../logging'; import { JsiiModule } from '../packaging'; import { PackageInfo, Target, TargetOptions, findLocalBuildDirs, } from '../target'; import { shell, Scratch, setExtend, filterAsync } from '../util'; import { DotNetGenerator } from './dotnet/dotnetgenerator'; import { toReleaseVersion } from './version-utils'; import { TargetName } from '.'; export const TARGET_FRAMEWORK = 'netcoreapp3.1'; /** * Build .NET packages all together, by generating an aggregate solution file */ export class DotnetBuilder implements TargetBuilder { private readonly targetName = 'dotnet'; public constructor( private readonly modules: readonly JsiiModule[], private readonly options: BuildOptions, ) {} public async buildModules(): Promise<void> { if (this.modules.length === 0) { return; } if (this.options.codeOnly) { // Simple, just generate code to respective output dirs await Promise.all( this.modules.map((module) => this.generateModuleCode( module, this.outputDir(module.outputDirectory), ), ), ); return; } // Otherwise make a single tempdir to hold all sources, build them together and copy them back out const scratchDirs: Array<Scratch<any>> = []; try { const tempSourceDir = await this.generateAggregateSourceDir(this.modules); scratchDirs.push(tempSourceDir); // Build solution logging.debug('Building .NET'); await shell( 'dotnet', ['build', '--force', '--configuration', 'Release'], { cwd: tempSourceDir.directory, retry: { maxAttempts: 5 }, }, ); await this.copyOutArtifacts(tempSourceDir.object); if (this.options.clean) { await Scratch.cleanupAll(scratchDirs); } } catch (e) { logging.warn( `Exception occurred, not cleaning up ${scratchDirs .map((s) => s.directory) .join(', ')}`, ); throw e; } } private async generateAggregateSourceDir( modules: readonly JsiiModule[], ): Promise<Scratch<TemporaryDotnetPackage[]>> { return Scratch.make(async (tmpDir: string) => { logging.debug(`Generating aggregate .NET source dir at ${tmpDir}`); const csProjs = []; const ret: TemporaryDotnetPackage[] = []; // Code generator will make its own subdirectory const generatedModules = modules.map((mod) => this.generateModuleCode(mod, tmpDir).then(() => mod), ); for (const mod of await Promise.all(generatedModules)) { const loc = projectLocation(mod); csProjs.push(loc.projectFile); ret.push({ outputTargetDirectory: mod.outputDirectory, artifactsDir: path.join(tmpDir, loc.projectDir, 'bin', 'Release'), }); } // Use 'dotnet' command line tool to build a solution file from these csprojs await shell('dotnet', ['new', 'sln', '-n', 'JsiiBuild'], { cwd: tmpDir }); await shell('dotnet', ['sln', 'add', ...csProjs], { cwd: tmpDir }); await this.generateNuGetConfigForLocalDeps(tmpDir); return ret; }); } private async copyOutArtifacts(packages: TemporaryDotnetPackage[]) { logging.debug('Copying out .NET artifacts'); await Promise.all(packages.map(copyOutIndividualArtifacts.bind(this))); async function copyOutIndividualArtifacts( this: DotnetBuilder, pkg: TemporaryDotnetPackage, ) { const targetDirectory = this.outputDir(pkg.outputTargetDirectory); await fs.mkdirp(targetDirectory); await fs.copy(pkg.artifactsDir, targetDirectory, { recursive: true, filter: (_, dst) => { return dst !== path.join(targetDirectory, TARGET_FRAMEWORK); }, }); } } private async generateModuleCode( module: JsiiModule, where: string, ): Promise<void> { const target = this.makeTarget(module); logging.debug(`Generating ${this.targetName} code into ${where}`); await target.generateCode(where, module.tarball); } /** * Decide whether or not to append 'dotnet' to the given output directory */ private outputDir(declaredDir: string) { return this.options.languageSubdirectory ? path.join(declaredDir, this.targetName) : declaredDir; } /** * Write a NuGet.config that will include build directories for local packages not in the current build * */ private async generateNuGetConfigForLocalDeps(where: string): Promise<void> { // Traverse the dependency graph of this module and find all modules that have // an <outdir>/dotnet directory. We will add those as local NuGet repositories. // This enables building against local modules. const allDepsOutputDirs = new Set<string>(); const resolvedModules = this.modules.map(async (module) => ({ module, localBuildDirs: await findLocalBuildDirs( module.moduleDirectory, this.targetName, ), })); for (const { module, localBuildDirs } of await Promise.all( resolvedModules, )) { setExtend(allDepsOutputDirs, localBuildDirs); // Also include output directory where we're building to, in case we build multiple packages into // the same output directory. allDepsOutputDirs.add(this.outputDir(module.outputDirectory)); } const localRepos = Array.from(allDepsOutputDirs); // If dotnet-runtime is checked-out and we can find a local repository, add it to the list. try { // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports,import/no-extraneous-dependencies const jsiiDotNetRuntime = require('@jsii/dotnet-runtime'); logging.info( `Using local version of the DotNet jsii runtime package at: ${jsiiDotNetRuntime.repository}`, ); localRepos.push(jsiiDotNetRuntime.repository); } catch { // Couldn't locate @jsii/dotnet-runtime, which is owkay! } // Filter out nonexistant directories, .NET will be unhappy if paths don't exist const existingLocalRepos = await filterAsync(localRepos, fs.pathExists); logging.debug('local NuGet repos:', existingLocalRepos); // Construct XML content. const configuration = xmlbuilder.create('configuration', { encoding: 'UTF-8', }); const packageSources = configuration.ele('packageSources'); const nugetOrgAdd = packageSources.ele('add'); nugetOrgAdd.att('key', 'nuget.org'); nugetOrgAdd.att('value', 'https://api.nuget.org/v3/index.json'); nugetOrgAdd.att('protocolVersion', '3'); existingLocalRepos.forEach((repo, index) => { const add = packageSources.ele('add'); add.att('key', `local-${index}`); add.att('value', path.join(repo)); }); if (this.options.arguments['dotnet-nuget-global-packages-folder']) { // Ensure we're not using the configured cache folder configuration .ele('config') .ele('add') .att('key', 'globalPackagesFolder') .att( 'value', path.resolve( this.options.arguments['dotnet-nuget-global-packages-folder'], '.nuget', 'packages', ), ); } const xml = configuration.end({ pretty: true }); // Write XML content to NuGet.config. const filePath = path.join(where, 'NuGet.config'); logging.debug(`Generated ${filePath}`); await fs.writeFile(filePath, xml); } private makeTarget(module: JsiiModule): Dotnet { return new Dotnet( { arguments: this.options.arguments, assembly: module.assembly, fingerprint: this.options.fingerprint, force: this.options.force, packageDir: module.moduleDirectory, rosetta: this.options.rosetta, runtimeTypeChecking: this.options.runtimeTypeChecking, targetName: this.targetName, }, this.modules.map((m) => m.name), ); } } interface TemporaryDotnetPackage { /** * Where the artifacts will be stored after build (relative to build dir) */ artifactsDir: string; /** * Where the artifacts ought to go for this particular module */ outputTargetDirectory: string; } function projectLocation(module: JsiiModule) { const packageId: string = module.assembly.targets!.dotnet!.packageId; return { projectDir: packageId, projectFile: path.join(packageId, `${packageId}.csproj`), }; } export default class Dotnet extends Target { public static toPackageInfos(assm: spec.Assembly): { [language: string]: PackageInfo; } { const packageId = assm.targets!.dotnet!.packageId; const version = toReleaseVersion(assm.version, TargetName.DOTNET); const packageInfo: PackageInfo = { repository: 'Nuget', url: `https://www.nuget.org/packages/${packageId}/${version}`, usage: { csproj: { language: 'xml', code: `<PackageReference Include="${packageId}" Version="${version}" />`, }, dotnet: { language: 'console', code: `dotnet add package ${packageId} --version ${version}`, }, 'packages.config': { language: 'xml', code: `<package id="${packageId}" version="${version}" />`, }, }, }; return { 'C#': packageInfo }; } public static toNativeReference(_type: spec.Type, options: any) { return { 'c#': `using ${options.namespace};`, }; } protected readonly generator: DotNetGenerator; public constructor( options: TargetOptions, assembliesCurrentlyBeingCompiled: string[], ) { super(options); this.generator = new DotNetGenerator( assembliesCurrentlyBeingCompiled, options, ); } // eslint-disable-next-line @typescript-eslint/require-await public async build(_sourceDir: string, _outDir: string): Promise<void> { throw new Error('Should not be called; use builder instead'); } /* eslint-enable @typescript-eslint/require-await */ }