/*
* Copyright 2014-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.ocaml;
import com.facebook.buck.cxx.CxxPreprocessorInput;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.args.Arg;
import com.facebook.buck.rules.args.StringArg;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepExecutionResult;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
/** A step that preprocesses, compiles, and assembles OCaml sources. */
public class OcamlBuildStep implements Step {
private final SourcePathResolver resolver;
private final ProjectFilesystem filesystem;
private final OcamlBuildContext ocamlContext;
private final ImmutableMap<String, String> cCompilerEnvironment;
private final ImmutableList<String> cCompiler;
private final ImmutableMap<String, String> cxxCompilerEnvironment;
private final ImmutableList<String> cxxCompiler;
private final boolean bytecodeOnly;
private final boolean hasGeneratedSources;
private final OcamlDepToolStep depToolStep;
public OcamlBuildStep(
SourcePathResolver resolver,
ProjectFilesystem filesystem,
OcamlBuildContext ocamlContext,
ImmutableMap<String, String> cCompilerEnvironment,
ImmutableList<String> cCompiler,
ImmutableMap<String, String> cxxCompilerEnvironment,
ImmutableList<String> cxxCompiler,
boolean bytecodeOnly) {
this.resolver = resolver;
this.filesystem = filesystem;
this.ocamlContext = ocamlContext;
this.cCompilerEnvironment = cCompilerEnvironment;
this.cCompiler = cCompiler;
this.cxxCompilerEnvironment = cxxCompilerEnvironment;
this.cxxCompiler = cxxCompiler;
this.bytecodeOnly = bytecodeOnly;
hasGeneratedSources =
ocamlContext.getLexInput().size() > 0 || ocamlContext.getYaccInput().size() > 0;
this.depToolStep =
new OcamlDepToolStep(
filesystem.getRootPath(),
this.ocamlContext.getSourcePathResolver(),
this.ocamlContext.getOcamlDepTool().get(),
ocamlContext.getMLInput(),
this.ocamlContext.getIncludeFlags(/* isBytecode */ false, /* excludeDeps */ true));
}
@Override
public String getShortName() {
return "OCaml compile";
}
@Override
public String getDescription(ExecutionContext context) {
return depToolStep.getDescription(context);
}
@Override
public StepExecutionResult execute(ExecutionContext context)
throws IOException, InterruptedException {
if (hasGeneratedSources) {
StepExecutionResult genExecutionResult = generateSources(context, filesystem.getRootPath());
if (!genExecutionResult.isSuccess()) {
return genExecutionResult;
}
}
StepExecutionResult depToolExecutionResult = depToolStep.execute(context);
if (!depToolExecutionResult.isSuccess()) {
return depToolExecutionResult;
}
// OCaml requires module A to be present in command line to ocamlopt or ocamlc before
// module B if B depends on A. In OCaml circular dependencies are prohibited, so all
// dependency relations among modules form DAG. Topologically sorting this graph satisfies the
// requirement.
//
// To get the DAG we launch ocamldep tool which provides the direct dependency information, like
// module A depends on modules B, C, D.
ImmutableList<Path> sortedInput =
sortDependency(
depToolStep.getStdout(),
ocamlContext.getSourcePathResolver().getAllAbsolutePaths(ocamlContext.getMLInput()));
ImmutableList.Builder<Path> nativeLinkerInputs = ImmutableList.builder();
if (!bytecodeOnly) {
StepExecutionResult mlCompileNativeExecutionResult =
executeMLNativeCompilation(
context, filesystem.getRootPath(), sortedInput, nativeLinkerInputs);
if (!mlCompileNativeExecutionResult.isSuccess()) {
return mlCompileNativeExecutionResult;
}
}
ImmutableList.Builder<Path> bytecodeLinkerInputs = ImmutableList.builder();
StepExecutionResult mlCompileBytecodeExecutionResult =
executeMLBytecodeCompilation(
context, filesystem.getRootPath(), sortedInput, bytecodeLinkerInputs);
if (!mlCompileBytecodeExecutionResult.isSuccess()) {
return mlCompileBytecodeExecutionResult;
}
ImmutableList.Builder<Path> cLinkerInputs = ImmutableList.builder();
StepExecutionResult cCompileExecutionResult = executeCCompilation(context, cLinkerInputs);
if (!cCompileExecutionResult.isSuccess()) {
return cCompileExecutionResult;
}
ImmutableList<Path> cObjects = cLinkerInputs.build();
if (!bytecodeOnly) {
nativeLinkerInputs.addAll(cObjects);
StepExecutionResult nativeLinkExecutionResult =
executeNativeLinking(context, nativeLinkerInputs.build());
if (!nativeLinkExecutionResult.isSuccess()) {
return nativeLinkExecutionResult;
}
}
bytecodeLinkerInputs.addAll(cObjects);
StepExecutionResult bytecodeLinkExecutionResult =
executeBytecodeLinking(context, bytecodeLinkerInputs.build());
if (!bytecodeLinkExecutionResult.isSuccess()) {
return bytecodeLinkExecutionResult;
}
if (!ocamlContext.isLibrary()) {
Step debugLauncher =
new OcamlDebugLauncherStep(
filesystem,
resolver,
new OcamlDebugLauncherStep.Args(
ocamlContext.getOcamlDebug().get(),
ocamlContext.getBytecodeOutput(),
ocamlContext.getOcamlInput(),
ocamlContext.getBytecodeIncludeFlags()));
return debugLauncher.execute(context);
} else {
return StepExecutionResult.SUCCESS;
}
}
private StepExecutionResult executeCCompilation(
ExecutionContext context, ImmutableList.Builder<Path> linkerInputs)
throws IOException, InterruptedException {
ImmutableList.Builder<String> cCompileFlags = ImmutableList.builder();
cCompileFlags.addAll(ocamlContext.getCCompileFlags());
cCompileFlags.addAll(ocamlContext.getCommonCFlags());
CxxPreprocessorInput cxxPreprocessorInput = ocamlContext.getCxxPreprocessorInput();
for (SourcePath cSrc : ocamlContext.getCInput()) {
Path outputPath = ocamlContext.getCOutput(resolver.getAbsolutePath(cSrc));
linkerInputs.add(outputPath);
Step compileStep =
new OcamlCCompileStep(
resolver,
filesystem.getRootPath(),
new OcamlCCompileStep.Args(
cCompilerEnvironment,
cCompiler,
ocamlContext.getOcamlCompiler().get(),
ocamlContext.getOcamlInteropIncludesDir(),
outputPath,
cSrc,
cCompileFlags.build(),
cxxPreprocessorInput.getIncludes()));
StepExecutionResult compileExecutionResult = compileStep.execute(context);
if (!compileExecutionResult.isSuccess()) {
return compileExecutionResult;
}
}
return StepExecutionResult.SUCCESS;
}
private StepExecutionResult executeNativeLinking(
ExecutionContext context, ImmutableList<Path> linkerInputs)
throws IOException, InterruptedException {
ImmutableList.Builder<Arg> flags = ImmutableList.builder();
flags.addAll(ocamlContext.getFlags());
flags.addAll(StringArg.from(ocamlContext.getCommonCLinkerFlags()));
OcamlLinkStep linkStep =
OcamlLinkStep.create(
filesystem.getRootPath(),
cxxCompilerEnvironment,
cxxCompiler,
ocamlContext.getOcamlCompiler().get().getCommandPrefix(resolver),
flags.build(),
ocamlContext.getOcamlInteropIncludesDir(),
ocamlContext.getNativeOutput(),
ocamlContext.getNativeLinkableInput().getArgs(),
ocamlContext.getCLinkableInput().getArgs(),
linkerInputs,
ocamlContext.isLibrary(),
/* isBytecode */ false,
resolver);
return linkStep.execute(context);
}
private StepExecutionResult executeBytecodeLinking(
ExecutionContext context, ImmutableList<Path> linkerInputs)
throws IOException, InterruptedException {
ImmutableList.Builder<Arg> flags = ImmutableList.builder();
flags.addAll(ocamlContext.getFlags());
flags.addAll(StringArg.from(ocamlContext.getCommonCLinkerFlags()));
OcamlLinkStep linkStep =
OcamlLinkStep.create(
filesystem.getRootPath(),
cxxCompilerEnvironment,
cxxCompiler,
ocamlContext.getOcamlBytecodeCompiler().get().getCommandPrefix(resolver),
flags.build(),
ocamlContext.getOcamlInteropIncludesDir(),
ocamlContext.getBytecodeOutput(),
ocamlContext.getBytecodeLinkableInput().getArgs(),
ocamlContext.getCLinkableInput().getArgs(),
linkerInputs,
ocamlContext.isLibrary(),
/* isBytecode */ true,
resolver);
return linkStep.execute(context);
}
private ImmutableList<Arg> getCompileFlags(boolean isBytecode, boolean excludeDeps) {
String output =
isBytecode
? ocamlContext.getCompileBytecodeOutputDir().toString()
: ocamlContext.getCompileNativeOutputDir().toString();
ImmutableList.Builder<Arg> flagBuilder = ImmutableList.builder();
flagBuilder.addAll(
StringArg.from(ocamlContext.getIncludeFlags(isBytecode, /* excludeDeps */ excludeDeps)));
flagBuilder.addAll(ocamlContext.getFlags());
flagBuilder.add(StringArg.of(OcamlCompilables.OCAML_INCLUDE_FLAG), StringArg.of(output));
return flagBuilder.build();
}
private StepExecutionResult executeMLNativeCompilation(
ExecutionContext context,
Path workingDirectory,
ImmutableList<Path> sortedInput,
ImmutableList.Builder<Path> linkerInputs)
throws IOException, InterruptedException {
for (Step step :
MakeCleanDirectoryStep.of(filesystem, ocamlContext.getCompileNativeOutputDir())) {
StepExecutionResult mkDirExecutionResult = step.execute(context);
if (!mkDirExecutionResult.isSuccess()) {
return mkDirExecutionResult;
}
}
for (Path inputOutput : sortedInput) {
String inputFileName = inputOutput.getFileName().toString();
String outputFileName =
inputFileName
.replaceFirst(OcamlCompilables.OCAML_ML_REGEX, OcamlCompilables.OCAML_CMX)
.replaceFirst(OcamlCompilables.OCAML_RE_REGEX, OcamlCompilables.OCAML_CMX)
.replaceFirst(OcamlCompilables.OCAML_MLI_REGEX, OcamlCompilables.OCAML_CMI)
.replaceFirst(OcamlCompilables.OCAML_REI_REGEX, OcamlCompilables.OCAML_CMI);
Path outputPath = ocamlContext.getCompileNativeOutputDir().resolve(outputFileName);
if (!outputFileName.endsWith(OcamlCompilables.OCAML_CMI)) {
linkerInputs.add(outputPath);
}
final ImmutableList<Arg> compileFlags =
getCompileFlags(/* isBytecode */ false, /* excludeDeps */ false);
Step compileStep =
new OcamlMLCompileStep(
workingDirectory,
resolver,
new OcamlMLCompileStep.Args(
filesystem::resolve,
cCompilerEnvironment,
cCompiler,
ocamlContext.getOcamlCompiler().get(),
ocamlContext.getOcamlInteropIncludesDir(),
outputPath,
inputOutput,
compileFlags));
StepExecutionResult compileExecutionResult = compileStep.execute(context);
if (!compileExecutionResult.isSuccess()) {
return compileExecutionResult;
}
}
return StepExecutionResult.SUCCESS;
}
private StepExecutionResult executeMLBytecodeCompilation(
ExecutionContext context,
Path workingDirectory,
ImmutableList<Path> sortedInput,
ImmutableList.Builder<Path> linkerInputs)
throws IOException, InterruptedException {
for (Step step :
MakeCleanDirectoryStep.of(filesystem, ocamlContext.getCompileBytecodeOutputDir())) {
StepExecutionResult mkDirExecutionResult = step.execute(context);
if (!mkDirExecutionResult.isSuccess()) {
return mkDirExecutionResult;
}
}
for (Path inputOutput : sortedInput) {
String inputFileName = inputOutput.getFileName().toString();
String outputFileName =
inputFileName
.replaceFirst(OcamlCompilables.OCAML_ML_REGEX, OcamlCompilables.OCAML_CMO)
.replaceFirst(OcamlCompilables.OCAML_RE_REGEX, OcamlCompilables.OCAML_CMO)
.replaceFirst(OcamlCompilables.OCAML_MLI_REGEX, OcamlCompilables.OCAML_CMI)
.replaceFirst(OcamlCompilables.OCAML_REI_REGEX, OcamlCompilables.OCAML_CMI);
Path outputPath = ocamlContext.getCompileBytecodeOutputDir().resolve(outputFileName);
if (!outputFileName.endsWith(OcamlCompilables.OCAML_CMI)) {
linkerInputs.add(outputPath);
}
final ImmutableList<Arg> compileFlags =
getCompileFlags(/* isBytecode */ true, /* excludeDeps */ false);
Step compileBytecodeStep =
new OcamlMLCompileStep(
workingDirectory,
resolver,
new OcamlMLCompileStep.Args(
filesystem::resolve,
cCompilerEnvironment,
cCompiler,
ocamlContext.getOcamlBytecodeCompiler().get(),
ocamlContext.getOcamlInteropIncludesDir(),
outputPath,
inputOutput,
compileFlags));
StepExecutionResult compileExecutionResult = compileBytecodeStep.execute(context);
if (!compileExecutionResult.isSuccess()) {
return compileExecutionResult;
}
}
return StepExecutionResult.SUCCESS;
}
private StepExecutionResult generateSources(ExecutionContext context, Path workingDirectory)
throws IOException, InterruptedException {
for (Step step : MakeCleanDirectoryStep.of(filesystem, ocamlContext.getGeneratedSourceDir())) {
StepExecutionResult mkDirExecutionResult = step.execute(context);
if (!mkDirExecutionResult.isSuccess()) {
return mkDirExecutionResult;
}
}
for (SourcePath yaccSource : ocamlContext.getYaccInput()) {
SourcePath output = ocamlContext.getYaccOutput(ImmutableSet.of(yaccSource)).get(0);
OcamlYaccStep yaccStep =
new OcamlYaccStep(
workingDirectory,
resolver,
new OcamlYaccStep.Args(
ocamlContext.getYaccCompiler().get(),
resolver.getAbsolutePath(output),
resolver.getAbsolutePath(yaccSource)));
StepExecutionResult yaccExecutionResult = yaccStep.execute(context);
if (!yaccExecutionResult.isSuccess()) {
return yaccExecutionResult;
}
}
for (SourcePath lexSource : ocamlContext.getLexInput()) {
SourcePath output = ocamlContext.getLexOutput(ImmutableSet.of(lexSource)).get(0);
OcamlLexStep lexStep =
new OcamlLexStep(
workingDirectory,
resolver,
new OcamlLexStep.Args(
ocamlContext.getLexCompiler().get(),
resolver.getAbsolutePath(output),
resolver.getAbsolutePath(lexSource)));
StepExecutionResult lexExecutionResult = lexStep.execute(context);
if (!lexExecutionResult.isSuccess()) {
return lexExecutionResult;
}
}
return StepExecutionResult.SUCCESS;
}
private ImmutableList<Path> sortDependency(
String depOutput, ImmutableSet<Path> mlInput) { // NOPMD doesn't understand method reference
OcamlDependencyGraphGenerator graphGenerator = new OcamlDependencyGraphGenerator();
return FluentIterable.from(graphGenerator.generate(depOutput))
.transform(Paths::get)
// The output of generate needs to be filtered as .cmo dependencies
// are generated as both .ml and .re files.
.filter(mlInput::contains)
.toList();
}
}