mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 14:04:04 -05:00
331 lines
13 KiB
TypeScript
331 lines
13 KiB
TypeScript
// Copyright (c) 2023, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import _ from 'underscore';
|
|
|
|
import type {ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
|
|
import {CompilationResult, ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js';
|
|
import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
|
|
import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
|
|
import type {SelectedLibraryVersion} from '../../types/libraries/libraries.interfaces.js';
|
|
import {unwrap} from '../assert.js';
|
|
import {BaseCompiler, SimpleOutputFilenameCompiler} from '../base-compiler.js';
|
|
import {CompilationEnvironment} from '../compilation-env.js';
|
|
import {logger} from '../logger.js';
|
|
|
|
import * as utils from '../utils.js';
|
|
import {JavaCompiler} from './java.js';
|
|
import {KotlinCompiler} from './kotlin.js';
|
|
|
|
export class D8Compiler extends BaseCompiler implements SimpleOutputFilenameCompiler {
|
|
static get key() {
|
|
return 'android-d8';
|
|
}
|
|
|
|
lineNumberRegex: RegExp;
|
|
methodEndRegex: RegExp;
|
|
|
|
minApiArgRegex: RegExp;
|
|
|
|
jvmSyspropArgRegex: RegExp;
|
|
syspropArgRegex: RegExp;
|
|
|
|
javaId: string;
|
|
kotlinId: string;
|
|
|
|
versionFromPropsRegex: RegExp;
|
|
versionFromJarRegex: RegExp;
|
|
|
|
libPaths: string[];
|
|
|
|
constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) {
|
|
super({...compilerInfo}, env);
|
|
|
|
this.lineNumberRegex = /^\s+\.line\s+(\d+).*$/;
|
|
this.methodEndRegex = /^\s*\.end\smethod.*$/;
|
|
|
|
this.minApiArgRegex = /^--min-api$/;
|
|
|
|
this.jvmSyspropArgRegex = /^-J.*$/;
|
|
this.syspropArgRegex = /^-D.*$/;
|
|
|
|
// TODO(#7150) this can be rephrased once 7150 is done...
|
|
this.javaId = this.compilerProps<string>(`compiler.${this.compiler.id}.javaId`);
|
|
if (!this.javaId) {
|
|
this.javaId = this.compilerProps<string>(`group.${this.compiler.group}.javaId`);
|
|
}
|
|
|
|
this.versionFromPropsRegex = /^version\.version=(.*)$/;
|
|
this.versionFromJarRegex = /^.*r8-(.*)\.jar$/;
|
|
|
|
this.kotlinId = this.compilerProps<string>(`group.${this.compiler.group}.kotlinId`);
|
|
|
|
this.libPaths = [];
|
|
}
|
|
|
|
override getOutputFilename(dirPath: string) {
|
|
return path.join(dirPath, `${path.basename(this.compileFilename, this.lang.extensions[0])}.dex`);
|
|
}
|
|
|
|
override async runCompiler(
|
|
compiler: string,
|
|
options: string[],
|
|
inputFilename: string,
|
|
execOptions: ExecutionOptionsWithEnv,
|
|
filters?: ParseFiltersAndOutputOptions,
|
|
): Promise<CompilationResult> {
|
|
const preliminaryCompilePath = path.dirname(inputFilename);
|
|
let outputFilename = '';
|
|
let initialResult: CompilationResult | null = null;
|
|
|
|
const javaCompiler = this.env.findCompiler('java', this.javaId) as JavaCompiler | undefined;
|
|
if (!javaCompiler) {
|
|
return {
|
|
...this.handleUserError(
|
|
{message: `Compiler ${this.lang.id} ${this.javaId} not configured correctly`},
|
|
'',
|
|
),
|
|
timedOut: false,
|
|
};
|
|
}
|
|
|
|
// Instantiate Java or Kotlin compiler based on the current language.
|
|
if (this.lang.id === 'android-java') {
|
|
outputFilename = javaCompiler.getOutputFilename(preliminaryCompilePath);
|
|
const javaOptions = _.compact(
|
|
javaCompiler.prepareArguments(
|
|
this.getClasspathArgument(),
|
|
javaCompiler.getDefaultFilters(),
|
|
{}, // backendOptions
|
|
inputFilename,
|
|
outputFilename,
|
|
[], // libraries
|
|
[], // overrides
|
|
),
|
|
);
|
|
initialResult = await javaCompiler.runCompiler(
|
|
javaCompiler.getInfo().exe,
|
|
javaOptions,
|
|
this.filename(inputFilename),
|
|
javaCompiler.getDefaultExecOptions(),
|
|
);
|
|
} else if (this.lang.id === 'android-kotlin') {
|
|
const kotlinCompiler = unwrap(this.env.findCompiler('kotlin', this.kotlinId)) as KotlinCompiler;
|
|
outputFilename = kotlinCompiler.getOutputFilename(preliminaryCompilePath);
|
|
const kotlinOptions = _.compact(
|
|
kotlinCompiler.prepareArguments(
|
|
this.getClasspathArgument(),
|
|
kotlinCompiler.getDefaultFilters(),
|
|
{}, // backendOptions
|
|
inputFilename,
|
|
outputFilename,
|
|
[], // libraries
|
|
[], // overrides
|
|
),
|
|
);
|
|
initialResult = await kotlinCompiler.runCompiler(
|
|
kotlinCompiler.getInfo().exe,
|
|
kotlinOptions,
|
|
this.filename(inputFilename),
|
|
kotlinCompiler.getDefaultExecOptions(),
|
|
);
|
|
} else {
|
|
logger.error('Language is neither android-java nor android-kotlin.');
|
|
}
|
|
|
|
// D8 should not run if initial compile stage failed, the JavaCompiler
|
|
// result can be returned instead.
|
|
if (initialResult && initialResult.code !== 0) {
|
|
return initialResult;
|
|
}
|
|
|
|
if (!execOptions) {
|
|
execOptions = this.getDefaultExecOptions();
|
|
}
|
|
if (!execOptions.customCwd) {
|
|
execOptions.customCwd = path.dirname(inputFilename);
|
|
}
|
|
|
|
let useDefaultMinApi = true;
|
|
|
|
// The items in 'options' before the source file are user inputs.
|
|
const sourceFileOptionIndex = options.findIndex(option => {
|
|
return option.endsWith('.java') || option.endsWith('.kt');
|
|
});
|
|
let userOptions = options.slice(0, sourceFileOptionIndex);
|
|
const syspropOptions: string[] = [];
|
|
for (const option of userOptions) {
|
|
if (this.minApiArgRegex.test(option)) {
|
|
useDefaultMinApi = false;
|
|
} else if (this.jvmSyspropArgRegex.test(option)) {
|
|
syspropOptions.push(option.replace('-J', '-'));
|
|
} else if (this.syspropArgRegex.test(option)) {
|
|
syspropOptions.push(option);
|
|
}
|
|
}
|
|
userOptions = userOptions.filter(
|
|
option => !this.jvmSyspropArgRegex.test(option) && !this.syspropArgRegex.test(option),
|
|
);
|
|
|
|
const files = await fs.readdir(preliminaryCompilePath, {encoding: 'utf8', recursive: true});
|
|
const classFiles = files.filter(f => f.endsWith('.class'));
|
|
const d8Options = [
|
|
...syspropOptions,
|
|
'-cp',
|
|
this.compiler.exe, // R8 jar.
|
|
'com.android.tools.r8.D8', // Main class name for the D8 compiler.
|
|
...userOptions,
|
|
...this.getMinApiArgument(useDefaultMinApi),
|
|
...classFiles,
|
|
];
|
|
const result = await this.exec(javaCompiler.javaRuntime, d8Options, execOptions);
|
|
return {
|
|
...this.transformToCompilationResult(result, outputFilename),
|
|
languageId: this.getCompilerResultLanguageId(filters),
|
|
};
|
|
}
|
|
|
|
async generateSmali(outputFilename: string, maxSize: number) {
|
|
const dirPath = path.dirname(outputFilename);
|
|
|
|
const javaCompiler = unwrap(this.env.findCompiler('java', this.javaId)) as JavaCompiler;
|
|
|
|
// There is only one dex file for all classes.
|
|
let files = await fs.readdir(dirPath);
|
|
const dexFile = files.find(f => f.endsWith('.dex'));
|
|
if (!dexFile) {
|
|
return (
|
|
'No output .dex file was found, but this is expected if you are using R8 without any keep annotations.\n' +
|
|
'Please load R8Example.java for an example with keep annotations.'
|
|
);
|
|
}
|
|
const baksmaliOptions = ['-jar', this.compiler.objdumper, 'd', `${dexFile}`, '--code-offsets', '-o', dirPath];
|
|
const execResult = await this.exec(javaCompiler.javaRuntime, baksmaliOptions, {
|
|
maxOutput: maxSize,
|
|
customCwd: dirPath,
|
|
});
|
|
if (execResult.code !== 0) {
|
|
logger.warn(`baksmali failed: ${execResult.stderr}\n${execResult.stdout}`);
|
|
}
|
|
|
|
// There is one smali file for each class.
|
|
files = await fs.readdir(dirPath);
|
|
const smaliFiles = files.filter(f => f.endsWith('.smali'));
|
|
let objResult = '';
|
|
for (const smaliFile of smaliFiles) {
|
|
objResult = objResult.concat((await fs.readFile(path.join(dirPath, smaliFile), 'utf8')) + '\n\n');
|
|
}
|
|
return objResult;
|
|
}
|
|
|
|
override async objdump(outputFilename: string, result: any, maxSize: number) {
|
|
const objResult = await this.generateSmali(outputFilename, maxSize);
|
|
const asmResult: ParsedAsmResult = {
|
|
asm: [
|
|
{
|
|
text: objResult,
|
|
},
|
|
],
|
|
};
|
|
|
|
result.asm = asmResult.asm;
|
|
return result;
|
|
}
|
|
|
|
override optionsForFilter(filters: ParseFiltersAndOutputOptions) {
|
|
filters.binary = true;
|
|
return [];
|
|
}
|
|
|
|
// Map line numbers to lines.
|
|
override async processAsm(result): Promise<ParsedAsmResult> {
|
|
if (result.code !== 0) {
|
|
return {asm: [{text: result.asm, source: null}]};
|
|
}
|
|
const segments: ParsedAsmResultLine[] = [];
|
|
const asm = result.asm[0].text;
|
|
|
|
let lineNumber;
|
|
for (const l of asm.split(/\n/)) {
|
|
if (this.lineNumberRegex.test(l)) {
|
|
lineNumber = Number.parseInt(l.match(this.lineNumberRegex)[1], 10);
|
|
segments.push({text: l, source: null});
|
|
} else if (this.methodEndRegex.test(l)) {
|
|
lineNumber = null;
|
|
segments.push({text: l, source: null});
|
|
} else {
|
|
if (/\S/.test(l) && lineNumber) {
|
|
segments.push({text: l, source: {file: null, line: lineNumber}});
|
|
} else {
|
|
segments.push({text: l, source: null});
|
|
}
|
|
}
|
|
}
|
|
return {asm: segments};
|
|
}
|
|
|
|
getClasspathArgument(): string[] {
|
|
const libString = this.libPaths.join(':');
|
|
return libString ? ['-cp', libString] : [''];
|
|
}
|
|
|
|
getMinApiArgument(useDefaultMinApi: boolean): string[] {
|
|
return useDefaultMinApi ? ['--min-api', '27'] : [''];
|
|
}
|
|
|
|
override getIncludeArguments(libraries: SelectedLibraryVersion[], dirPath: string): string[] {
|
|
this.libPaths = libraries.flatMap(selectedLib => {
|
|
const foundVersion = this.findLibVersion(selectedLib);
|
|
if (!foundVersion) return [];
|
|
return foundVersion.path;
|
|
});
|
|
return this.libPaths;
|
|
}
|
|
|
|
override async getVersion() {
|
|
logger.info(`Gathering ${this.compiler.id} version information on ${this.compiler.exe}...`);
|
|
const versionFile = path.join(path.dirname(this.compiler.exe), 'r8-version.properties');
|
|
const versionInfo = await utils.tryReadTextFile(versionFile);
|
|
const versionCode = (() => {
|
|
if (versionInfo !== undefined) {
|
|
for (const l of versionInfo.split(/\n/)) {
|
|
if (this.versionFromPropsRegex.test(l)) {
|
|
return l.match(this.versionFromPropsRegex)![1];
|
|
}
|
|
}
|
|
throw new Error(`Unable to parse version info from ${versionFile}`);
|
|
}
|
|
// Non-latest R8 already has the version in the filename.
|
|
return this.compiler.exe.match(this.versionFromJarRegex)![1];
|
|
})();
|
|
return {
|
|
stdout: versionCode,
|
|
stderr: '',
|
|
code: 0,
|
|
};
|
|
}
|
|
}
|