github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/js/src/solts/build.ts (about)

     1  import { promises as fs } from 'fs';
     2  import * as path from 'path';
     3  import * as solcv5 from 'solc_v5';
     4  import * as solcv8 from 'solc_v8';
     5  import {
     6    decodeOutput,
     7    encodeInput,
     8    importLocalResolver,
     9    inputDescriptionFromFiles,
    10    Solidity,
    11  } from '../contracts/compile';
    12  import { Compiled, newFile, printNodes, tokenizeLinks } from './api';
    13  
    14  const solcCompilers = {
    15    v5: solcv5,
    16    v8: solcv8,
    17  } as const;
    18  
    19  export const defaultBuildOptions = {
    20    solcVersion: 'v5' as keyof typeof solcCompilers,
    21    burrowImportPath: (sourceFile: string) => '@hyperledger/burrow' as string,
    22    binPath: 'bin' as string | false,
    23    abiExt: '.abi' as string,
    24    // Used to resolve layout in bin folder - defaults to srcPath if is passed or process.cwd() otherwise
    25    basePath: undefined as undefined | string,
    26    failOnWarnings: false as boolean,
    27  } as const;
    28  
    29  export type BuildOptions = typeof defaultBuildOptions;
    30  
    31  /**
    32   * This is our Solidity -> Typescript code generation function, it:
    33   *  - Compiles Solidity source
    34   *  - Generates typescript code wrapping the Solidity contracts and functions that calls Burrow
    35   *  - Generates typescript code to deploy the contracts
    36   *  - Outputs the ABI files into bin to be later included in the distribution (for Vent and other ABI-consuming services)
    37   */
    38  export async function build(srcPathOrFiles: string | string[], opts?: Partial<BuildOptions>): Promise<void> {
    39    const { failOnWarnings, solcVersion, binPath, basePath, burrowImportPath, abiExt } = {
    40      ...defaultBuildOptions,
    41      ...opts,
    42    };
    43    const resolvedBasePath = basePath ?? (typeof srcPathOrFiles === 'string' ? srcPathOrFiles : process.cwd());
    44    process.chdir(resolvedBasePath);
    45    const basePathPrefix = new RegExp('^' + path.resolve(resolvedBasePath));
    46    const solidityFiles = await getSourceFilesList(srcPathOrFiles);
    47    const inputDescription = inputDescriptionFromFiles(
    48      // solidityFiles.map((f) => path.resolve(resolvedBasePath, f.replace(basePathPrefix, ''))),
    49      solidityFiles,
    50    );
    51    const input = encodeInput(inputDescription);
    52    const solc = solcCompilers[solcVersion];
    53  
    54    const solcOutput = solc.compile(input, { import: importLocalResolver(resolvedBasePath) });
    55    const output = decodeOutput(solcOutput);
    56    const errors = output.errors?.filter((e) => failOnWarnings || e.severity === 'error') || [];
    57    if (errors.length > 0) {
    58      throw new Error('Solidity compiler errors:\n' + formatErrors(errors));
    59    }
    60    const warnings = output.errors?.filter((e) => e.severity === 'warning') || [];
    61  
    62    if (warnings.length) {
    63      console.error('Solidity compiler warnings (not treated as fatal):\n' + formatErrors(warnings));
    64    }
    65  
    66    const plan = Object.keys(output.contracts).map((filename) => ({
    67      source: filename,
    68      target: filename.replace(/\.[^/.]+$/, '.abi.ts'),
    69      contracts: Object.entries(output.contracts[filename]).map(([name, contract]) => ({
    70        name,
    71        contract,
    72      })),
    73    }));
    74  
    75    let binPlan: { source: string; filename: string; abi: string }[] = [];
    76  
    77    if (binPath !== false) {
    78      await fs.mkdir(binPath, { recursive: true });
    79      binPlan = plan.flatMap((f) => {
    80        return f.contracts.map(({ name, contract }) => ({
    81          source: f.source,
    82          name,
    83          filename: path.join(binPath, path.dirname(path.resolve(f.source)).replace(basePathPrefix, ''), name + abiExt),
    84          abi: JSON.stringify(contract),
    85        }));
    86      });
    87  
    88      const dupes = findDupes(binPlan, (b) => b.filename);
    89  
    90      if (dupes.length) {
    91        const dupeDescs = dupes.map(({ key, dupes }) => ({ duplicate: key, sources: dupes.map((d) => d.source) }));
    92        throw Error(
    93          `Duplicate contract names found (these contracts will result ABI filenames that will collide since ABIs ` +
    94            `are flattened in '${binPath}'):\n${dupeDescs.map((d) => JSON.stringify(d)).join('\n')}`,
    95        );
    96      }
    97    }
    98  
    99    // Write the ABIs emitted for each file to the name of that file without extension. We flatten into a single
   100    // directory because that's what burrow deploy has always done.
   101    await Promise.all([
   102      ...binPlan.map(async ({ filename, abi }) => {
   103        await fs.mkdir(path.dirname(filename), { recursive: true });
   104        await fs.writeFile(filename, abi);
   105      }),
   106      ...plan.map(({ source, target, contracts }) =>
   107        fs.writeFile(
   108          target,
   109          printNodes(
   110            ...newFile(
   111              contracts.map(({ name, contract }) => getCompiled(name, contract)),
   112              burrowImportPath(source),
   113            ),
   114          ),
   115        ),
   116      ),
   117    ]);
   118  }
   119  
   120  function getCompiled(name: string, contract: Solidity.Contract): Compiled {
   121    return {
   122      name,
   123      abi: contract.abi,
   124      bytecode: contract.evm.bytecode.object,
   125      deployedBytecode: contract.evm.deployedBytecode.object,
   126      links: tokenizeLinks(contract.evm.bytecode.linkReferences),
   127    };
   128  }
   129  
   130  function findDupes<T>(list: T[], by: (t: T) => string): { key: string; dupes: T[] }[] {
   131    const grouped = list.reduce((acc, t) => {
   132      const k = by(t);
   133      if (!acc[k]) {
   134        acc[k] = [];
   135      }
   136      acc[k].push(t);
   137      return acc;
   138    }, {} as Record<string, T[]>);
   139    return Object.entries(grouped)
   140      .filter(([_, group]) => group.length > 1)
   141      .map(([key, dupes]) => ({
   142        key,
   143        dupes,
   144      }));
   145  }
   146  
   147  async function getSourceFilesList(srcPathOrFiles: string | string[]): Promise<string[]> {
   148    if (typeof srcPathOrFiles === 'string') {
   149      const files: string[] = [];
   150      for await (const f of walkDir(srcPathOrFiles)) {
   151        if (path.extname(f) === '.sol') {
   152          files.push(f);
   153        }
   154      }
   155      return files;
   156    }
   157    return srcPathOrFiles;
   158  }
   159  
   160  async function* walkDir(dir: string): AsyncGenerator<string, void, void> {
   161    for await (const d of await fs.opendir(dir)) {
   162      const entry = path.join(dir, d.name);
   163      if (d.isDirectory()) {
   164        yield* walkDir(entry);
   165      } else if (d.isFile()) {
   166        yield entry;
   167      }
   168    }
   169  }
   170  
   171  function formatErrors(errors: Solidity.Error[]): string {
   172    return errors.map((err) => err.formattedMessage || err.message).join('');
   173  }