
io.questdb.maven.rust.Crate Maven / Gradle / Ivy
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2023 QuestDB
*
* 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 io.questdb.maven.rust;
import io.questdb.jar.jni.Platform;
import io.questdb.jar.jni.PlatformConventions;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.tomlj.Toml;
import org.tomlj.TomlArray;
import org.tomlj.TomlInvalidTypeException;
import org.tomlj.TomlTable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executors;
/**
* Controls running tasks on a Rust crate.
*/
public class Crate {
private final Path crateRoot;
private final Path targetDir;
private final Params params;
private final TomlTable cargoToml;
private final String packageName;
private Log log;
public Crate(
Path crateRoot,
Path targetRootDir,
Params params) throws MojoExecutionException {
this.log = nullLog();
this.crateRoot = crateRoot;
this.targetDir = targetRootDir.resolve(getDirName());
this.params = params;
final Path tomlPath = crateRoot.resolve("Cargo.toml");
if (!Files.exists(tomlPath, LinkOption.NOFOLLOW_LINKS)) {
throw new MojoExecutionException(
"Cargo.toml file expected under: " + crateRoot);
}
try {
this.cargoToml = Toml.parse(tomlPath);
} catch (IOException e) {
throw new MojoExecutionException(
"Failed to parse Cargo.toml file: " + e.getMessage());
}
try {
packageName = cargoToml.getString("package.name");
if (packageName == null) {
throw new MojoExecutionException(
"Missing required `package.name` from Cargo.toml file");
}
} catch (TomlInvalidTypeException e) {
throw new MojoExecutionException(
"Failed to extract `package.name` from Cargo.toml file: " +
e.getMessage());
}
}
public static String pinLibName(String name) {
return PlatformConventions.LIB_PREFIX +
name.replace('-', '_') +
PlatformConventions.LIB_SUFFIX;
}
public static String pinBinName(String name) {
return name + PlatformConventions.EXE_SUFFIX;
}
public static Log nullLog() {
return new Log() {
@Override
public void debug(CharSequence content) {
}
@Override
public void debug(CharSequence content, Throwable error) {
}
@Override
public void debug(Throwable error) {
}
@Override
public void error(CharSequence content) {
}
@Override
public void error(CharSequence content, Throwable error) {
}
@Override
public void error(Throwable error) {
}
@Override
public void info(CharSequence content) {
}
@Override
public void info(CharSequence content, Throwable error) {
}
@Override
public void info(Throwable error) {
}
@Override
public boolean isDebugEnabled() {
return false;
}
@Override
public boolean isErrorEnabled() {
return false;
}
@Override
public boolean isInfoEnabled() {
return false;
}
@Override
public boolean isWarnEnabled() {
return false;
}
@Override
public void warn(CharSequence content) {
}
@Override
public void warn(CharSequence content, Throwable error) {
}
@Override
public void warn(Throwable error) {
}
};
}
public void setLog(Log log) {
this.log = log;
}
private String getDirName() {
return crateRoot.getFileName().toString();
}
private String getProfile() {
return params.release ? "release" : "debug";
}
public boolean hasCdylib() {
try {
TomlArray crateTypes = getCrateTypes();
if (crateTypes == null) {
return false;
}
for (int index = 0; index < crateTypes.size(); index++) {
String crateType = crateTypes.getString(index);
if ((crateType != null) && crateType.equals("cdylib")) {
return true;
}
}
return false;
} catch (TomlInvalidTypeException e) {
return false;
}
}
private TomlArray getCrateTypes() {
TomlArray crateTypes = cargoToml.getArray("lib.crate-type");
if (crateTypes == null) {
String crateTypeLegacyKey = "lib.crate_type";
return cargoToml.getArray(crateTypeLegacyKey);
}
return crateTypes;
}
private String getCdylibName() throws MojoExecutionException {
String name;
try {
name = cargoToml.getString("lib.name");
} catch (TomlInvalidTypeException e) {
throw new MojoExecutionException(
"Failed to extract `lib.name` from Cargo.toml file: " +
e.getMessage());
}
// The name might be missing, but the lib section might be present.
if ((name == null) && hasCdylib()) {
name = packageName;
}
return name;
}
private List getBinNames() throws MojoExecutionException {
final List binNames = new java.util.ArrayList<>();
String defaultBin = null;
if (Files.exists(crateRoot.resolve("src").resolve("main.rs"))) {
// Expecting default bin, given that there's no lib.
defaultBin = packageName;
binNames.add(defaultBin);
}
TomlArray bins;
try {
bins = cargoToml.getArray("bin");
} catch (TomlInvalidTypeException e) {
throw new MojoExecutionException(
"Failed to extract `bin`s from Cargo.toml file: " +
e.getMessage());
}
if (bins == null) {
return binNames;
}
for (int index = 0; index < bins.size(); ++index) {
final TomlTable bin = bins.getTable(index);
if (bin == null) {
throw new MojoExecutionException(
"Failed to extract `bin`s from Cargo.toml file: " +
"expected a `bin` table at index " + index);
}
String name;
try {
name = bin.getString("name");
} catch (TomlInvalidTypeException e) {
throw new MojoExecutionException(
"Failed to extract `bin`s from Cargo.toml file: " +
"expected a string at index " + index + " `name` key");
}
if (name == null) {
throw new MojoExecutionException(
"Failed to extract `bin`s from Cargo.toml file: " +
"missing `name` key at `bin` with index " + index);
}
String path;
try {
path = bin.getString("path");
} catch (TomlInvalidTypeException e) {
throw new MojoExecutionException(
"Failed to extract `bin`s from Cargo.toml file: " +
"expected a string at index " + index + " `path` key");
}
// Handle special case where the default bin is renamed.
if ((path != null) && path.equals("src/main.rs")) {
defaultBin = name;
binNames.remove(0);
binNames.add(0, defaultBin);
}
// This `[[bin]]` entry just configures the default bin.
// It's already been added.
if (!name.equals(defaultBin)) {
binNames.add(name);
}
}
return binNames;
}
public List getArtifactPaths() throws MojoExecutionException {
List paths = new ArrayList<>();
final String profile = getProfile();
final String libName = getCdylibName();
if (libName != null) {
final Path libPath = targetDir
.resolve(profile)
.resolve(pinLibName(libName));
paths.add(libPath);
}
for (String binName : getBinNames()) {
final Path binPath = targetDir
.resolve(profile)
.resolve(pinBinName(binName));
paths.add(binPath);
}
return paths;
}
private String getCargoPath() {
String path = params.cargoPath;
final boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");
// Expand "~" to user's home directory.
// This works around a limitation of ProcessBuilder.
if (!isWindows && path.startsWith("~/")) {
path = System.getProperty("user.home") + path.substring(1);
}
return path;
}
private void runCommand(List args)
throws IOException, InterruptedException, MojoExecutionException {
final ProcessBuilder processBuilder = new ProcessBuilder(args);
processBuilder.redirectErrorStream(true);
processBuilder.environment().putAll(params.environmentVariables);
// Set the current working directory for the cargo command.
processBuilder.directory(crateRoot.toFile());
final Process process = processBuilder.start();
Executors.newSingleThreadExecutor().submit(() ->
new BufferedReader(new InputStreamReader(process.getInputStream()))
.lines()
.forEach(log::info));
final int exitCode = process.waitFor();
if (exitCode != 0) {
throw new MojoExecutionException(
"Cargo command failed with exit code " + exitCode);
}
}
private void cargo(List args) throws MojoExecutionException, MojoFailureException {
String cargoPath = getCargoPath();
final List cmd = new ArrayList<>();
cmd.add(cargoPath);
cmd.addAll(args);
log.info("Working directory: " + crateRoot);
if (!params.environmentVariables.isEmpty()) {
log.info("Environment variables:");
for (String key : params.environmentVariables.keySet()) {
log.info(" " + key + "=" + Shlex.quote(
params.environmentVariables.get(key)));
}
}
log.info("Running: " + Shlex.quote(cmd));
try {
runCommand(cmd);
} catch (IOException | InterruptedException e) {
CargoInstalledChecker.INSTANCE.check(cargoPath);
throw new MojoFailureException("Failed to invoke cargo", e);
}
}
private void addCargoArgs(List args) {
if (params.verbosity != null) {
args.add(params.verbosity);
}
args.add("--target-dir");
args.add(targetDir.toAbsolutePath().toString());
if (params.release) {
args.add("--release");
}
if (params.allFeatures) {
args.add("--all-features");
}
if (params.noDefaultFeatures) {
args.add("--no-default-features");
}
final String[] cleanedFeatures = params.cleanedFeatures();
if (cleanedFeatures.length > 0) {
args.add("--features");
args.add(String.join(",", cleanedFeatures));
}
if (params.tests) {
args.add("--tests");
}
if (params.extraArgs != null) {
Collections.addAll(args, params.extraArgs);
}
}
public void build() throws MojoExecutionException, MojoFailureException {
List args = new ArrayList<>();
args.add("build");
addCargoArgs(args);
cargo(args);
}
public void test() throws MojoExecutionException, MojoFailureException {
List args = new ArrayList<>();
args.add("test");
addCargoArgs(args);
cargo(args);
}
private Path resolveCopyToDir() throws MojoExecutionException {
Path copyToDir = params.copyToDir;
if (copyToDir == null) {
return null;
}
if (params.copyWithPlatformDir) {
copyToDir = copyToDir.resolve(Platform.RESOURCE_PREFIX);
}
if (!Files.exists(copyToDir, LinkOption.NOFOLLOW_LINKS)) {
try {
Files.createDirectories(copyToDir);
} catch (IOException e) {
throw new MojoExecutionException(
"Failed to create directory " + copyToDir +
": " + e.getMessage(), e);
}
}
if (!Files.isDirectory(copyToDir)) {
throw new MojoExecutionException(copyToDir + " is not a directory");
}
return copyToDir;
}
public void copyArtifacts() throws MojoExecutionException {
// Cargo nightly has support for `--out-dir`
// which allows us to copy the artifacts directly to the desired path.
// Once the feature is stabilized, copy the artifacts directly via:
// args.add("--out-dir")
// args.add(resolveCopyToDir());
final Path copyToDir = resolveCopyToDir();
if (copyToDir == null) {
return;
}
final List artifactPaths = getArtifactPaths();
log.info(
"Copying " + getDirName() +
"'s artifacts to " + Shlex.quote(
copyToDir.toAbsolutePath().toString()));
for (Path artifactPath : artifactPaths) {
final Path fileName = artifactPath.getFileName();
final Path destPath = copyToDir.resolve(fileName);
try {
Files.copy(
artifactPath,
destPath,
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new MojoExecutionException(
"Failed to copy " + artifactPath +
" to " + copyToDir + ":" + e.getMessage());
}
log.info("Copied " + Shlex.quote(fileName.toString()));
}
}
public static class Params {
public String verbosity;
public HashMap environmentVariables;
public String cargoPath;
public boolean release;
public String[] features;
public boolean allFeatures;
public boolean noDefaultFeatures;
public boolean tests;
public String[] extraArgs;
public Path copyToDir;
public boolean copyWithPlatformDir;
/**
* Returns the features array with empty and null elements removed.
*/
public String[] cleanedFeatures() {
if ((features == null) || (features.length == 0)) {
return new String[0];
}
List cleanedFeatures = new ArrayList<>();
for (String feature : features) {
if (feature != null) {
feature = feature.trim();
if (!feature.isEmpty()) {
cleanedFeatures.add(feature);
}
}
}
return cleanedFeatures.toArray(new String[0]);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy