JavaBuilder/src/dorkbox/build/Project.java

1052 lines
31 KiB
Java

/*
* Copyright 2012 dorkbox, llc
*
* 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 dorkbox.build;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.minlog.Log;
import dorkbox.BuildOptions;
import dorkbox.BuildVersion;
import dorkbox.Builder;
import dorkbox.build.util.BuildLog;
import dorkbox.build.util.Hash;
import dorkbox.build.util.OutputFile;
import dorkbox.build.util.ShutdownHook;
import dorkbox.build.util.wildcard.Paths;
import dorkbox.license.License;
import dorkbox.util.FileUtil;
import dorkbox.util.OS;
import dorkbox.util.serialization.FileSerializer;
import dorkbox.util.serialization.SerializationManager;
import io.netty.buffer.ByteBuf;
@SuppressWarnings({"unchecked", "unused", "Convert2Diamond", "AccessStaticViaInstance"})
public abstract
class Project<T extends Project<T>> {
public static final String JAR_EXTENSION = ".jar";
public static final String SRC_EXTENSION = "_src.zip";
public static final String STAGING = "staging";
public static final String Java_Pattern = "**" + File.separator + "*.java";
public static final String Jar_Pattern = "**" + File.separator + "*.jar";
public static Map<String, Project> deps = new LinkedHashMap<String, Project>();
protected static Set<String> buildList = new HashSet<String>();
private static boolean forceRebuildAll = false;
private static boolean alreadyChecked = false;
private static Comparator<Project> dependencyComparator = new ProjectComparator();
public static List<File> builderFiles = new ArrayList<File>();
public static Thread shutdownHook;
static final SerializationManager manager = new SerializationManager() {
Kryo kryo = new Kryo();
{
// we don't want logging from Kryo...
Log.set(Log.LEVEL_ERROR);
register(File.class, new FileSerializer());
}
@Override
public
SerializationManager register(final Class<?> clazz) {
kryo.register(clazz);
return this;
}
@Override
public
SerializationManager register(final Class<?> clazz, final int id) {
kryo.register(clazz, id);
return this;
}
@Override
public
SerializationManager register(final Class<?> clazz, final Serializer<?> serializer) {
kryo.register(clazz, serializer);
return this;
}
@Override
public
SerializationManager register(final Class<?> type, final Serializer<?> serializer, final int id) {
kryo.register(type, serializer, id);
return this;
}
@Override
public
void write(final ByteBuf buffer, final Object message) {
final Output output = new Output();
writeFullClassAndObject(output, message);
buffer.writeBytes(output.getBuffer());
}
@Override
public
Object read(final ByteBuf buffer, final int length) throws IOException {
final Input input = new Input();
buffer.readBytes(input.getBuffer());
final Object o = readFullClassAndObject(input);
buffer.skipBytes(input.position());
return o;
}
@Override
public
void writeFullClassAndObject(final Output output, final Object value) {
kryo.writeClassAndObject(output, value);
}
@Override
public
Object readFullClassAndObject(final Input input) throws IOException {
return kryo.readClassAndObject(input);
}
@Override
public
void finishInit(final Logger logger, final Logger writeLogger) {
}
@Override
public
boolean initialized() {
return false;
}
};
static {
// check to see if our deploy code has changed. if yes, then we have to rebuild everything since
// we don't know what might have changed.
final Paths paths = new Paths();
File file = new File(Project.class.getSimpleName() + ".java").getAbsoluteFile().getParentFile();
paths.glob(file.getAbsolutePath(), Java_Pattern);
for (File f : builderFiles) {
paths.glob(f.getAbsolutePath(), Java_Pattern);
}
try {
String oldHash = Builder.settings.get("BUILD", String.class);
String hashedContents = Hash.generateChecksums(paths);
if (oldHash != null) {
if (!oldHash.equals(hashedContents)) {
forceRebuildAll = true;
}
}
else {
forceRebuildAll = true;
}
if (forceRebuildAll) {
if (!alreadyChecked) {
alreadyChecked = true;
BuildLog.println("Build system changed. Rebuilding.");
}
// we only want to save the project checksums ON EXIT (so version modifications/save() can be applied)!
shutdownHook = new Thread(new ShutdownHook(paths));
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
} catch (IOException e) {
e.printStackTrace();
}
Hash.forceRebuildAll = forceRebuildAll;
}
public static
Project<?> create(Project<?> project) {
deps.put(project.name, project);
return project;
}
// removes all saved checksums as well as dependencies. Used to "reset everything", similar to if it was relaunched.
public static
void reset() {
deps.clear();
buildList.clear();
BuildLog.start();
BuildLog.title("RESET").println("All project info resetting...");
BuildLog.finish();
}
public String name;
protected BuildVersion version;
protected File stagingDir;
// if true, we do not delete the older versions during a build
boolean keepOldVersion;
public OutputFile outputFile;
// could also be called native lib location
private String distLocation;
public Paths extraFiles = new Paths();
/** DIRECT dependencies for this project */
protected List<Project<?>> dependencies = new ArrayList<Project<?>>();
/** Dependencies that are just source code files, not a jar. These are converted into a jar when the project is build */
protected Paths sourceDependencies = new Paths();
protected Paths sourcePaths = new Paths();
/** ALL related dependencies for this project (ie: recursively searched) */
protected transient List<Project<?>> fullDependencyList = null;
// used to make sure licenses are called in the correct spot
private transient boolean calledLicenseBefore = false;
protected List<License> licenses = new ArrayList<License>();
protected BuildOptions buildOptions;
private ArrayList<String> unresolvedDependencies = new ArrayList<String>();
// used to suppress certain messages when building deps
protected transient boolean isBuildingDependencies = false;
// used to suppress logging what dependencies are used
private boolean suppressDependencyLog = false;
// Sometimes we don't want to export the build to maven (ie: when running a test, for example)
protected boolean exportToMaven = false;
MavenExporter mavenExporter;
public MavenInfo mavenInfo;
/** true if we had to build this project */
transient boolean shouldBuild = false;
/** true if we skipped building this project */
transient boolean skippedBuild = false;
// true if we should rebuild this project
boolean forceRebuild = false;
protected transient Hash hash;
/**
* Temporary projects are always built, but not always exported to maven (this is controlled by the parent, non-temp project
* recursively)
*/
public boolean temporary = false;
boolean overrideTemporary = false;
public String description;
public static
Project<?> get(String projectName) {
if (deps.containsKey(projectName)) {
Project<?> project = deps.get(projectName);
// put swt lib into jar!
return project;
}
else {
throw new IllegalArgumentException(projectName + " project must exist!");
}
}
public static
void buildAll() throws Exception {
// organize the list of items to build, so that our build order is at least SOMEWHAT in order,
// were the dependencies build first. This is just an optimization step
List<Project> sorted = new ArrayList<Project>(deps.values());
Collections.sort(sorted, dependencyComparator);
for (Project project : sorted) {
if (!(project instanceof ProjectJar)) {
project.build();
}
}
}
/**
* resolves all of the dependencies for this project, since the build order can be specified in ANY order
*/
protected
void resolveDeps() {
Iterator<String> iterator = this.unresolvedDependencies.iterator();
while (iterator.hasNext()) {
String unresolved = iterator.next();
Project<?> project = deps.get(unresolved);
if (project != null) {
this.dependencies.add(project);
iterator.remove();
}
}
}
public static
void build(String projectName) throws Exception {
Project<?> project = get(projectName);
if (project != null) {
project.build();
}
else {
System.err.println("Project is NULL. Aborting build.");
}
}
public static
void remove(String outputDir) {
deps.remove(outputDir);
}
// for serialization
protected
Project() {
}
protected
Project(String projectName) {
this.name = projectName;
this.buildOptions = new BuildOptions();
String lowerCase_outputDir = projectName.toLowerCase();
this.stagingDir = FileUtil.normalize(STAGING + File.separator + lowerCase_outputDir);
// must call this method, because it's not overridden by jar type
outputFile0(new File(this.stagingDir.getParentFile(), this.name + getExtension()).getAbsolutePath(), null);
hash = new Hash(projectName, buildOptions);
}
/**
* Builds using the current, detected JDK.
*
* @return true if this project was built, false otherwise
*/
public final
boolean build() throws IOException {
return build(OS.javaVersion);
}
/**
* Builds using the current, detected JDK.
*
* @return true if this project was built, false otherwise
*/
public abstract
boolean build(final int targetJavaVersion) throws IOException;
/**
* Exports this project to the maven central repository
*/
public
MavenExporter mavenExport(final String groupId, final MavenInfo.Scope scope) {
mavenExport(new MavenExporter(new MavenInfo(groupId, name, this.version.toString(), scope)));
return mavenExporter;
}
/**
* Exports this project to the maven central repository
*/
public
T mavenExport(MavenExporter exporter) {
mavenExporter = exporter;
return (T)this;
}
/**
* Specifies the specific maven info for this project, to configure dependencies
*/
public
T mavenInfo(final String groupId) {
mavenInfo = new MavenInfo(groupId, this.name, this.version.toString(), null); // null = Scope.COMPILE
return (T)this;
}
/**
* Specifies the specific maven info for this project, to configure dependencies
*/
public
T mavenInfo(final MavenInfo mavenInfo) {
this.mavenInfo = mavenInfo;
return (T)this;
}
/**
* Specifies the specific maven info for this project, to configure dependencies
*/
public
T mavenInfo(final String groupId, final MavenInfo.Scope scope) {
mavenInfo = new MavenInfo(groupId, this.name, this.version.toString(), scope);
return (T)this;
}
/**
* Specifies the specific maven info for this project, to configure dependencies
*/
public
T mavenInfo(final String groupId, final String artifactId, final MavenInfo.Scope scope) {
mavenInfo = new MavenInfo(groupId, artifactId, this.version.toString(), scope);
return (T)this;
}
/**
* Specifies the specific maven info for this project, to configure dependencies
*/
public
T mavenInfo(final String groupId, final String artifactId, final String version, final MavenInfo.Scope scope) {
mavenInfo = new MavenInfo(groupId, artifactId, version, scope);
return (T)this;
}
public
List<License> getLicenses() {
return this.licenses;
}
public abstract
String getExtension();
public
void getRecursiveLicenses(Set<License> licenses) {
licenses.addAll(this.licenses);
Set<Project<?>> deps = new HashSet<Project<?>>();
getRecursiveDependencies(deps);
for (Project<?> project : deps) {
project.getRecursiveLicenses(licenses);
}
}
public
void getRecursiveDependencies(Set<Project<?>> dependencies) {
for (Project<?> project : this.dependencies) {
dependencies.add(project);
project.getRecursiveDependencies(dependencies);
}
}
/**
* Checks to see if we already built this project. Also, will automatically build this project's
* dependencies (if they haven't already been built).
*/
protected
void resolveDependencies() {
resolveDeps();
if (fullDependencyList == null) {
// ONLY build the dependencies as well
HashSet<Project<?>> deps = new HashSet<Project<?>>();
getRecursiveDependencies(deps);
Project<?>[] array = new Project<?>[deps.size()];
deps.toArray(array);
fullDependencyList = Arrays.asList(array);
fullDependencyList.sort(dependencyComparator);
}
}
/**
* suppress logging what dependencies are used
*/
public
T suppressDependencyLog() {
suppressDependencyLog = true;
return (T) this;
}
/**
* Outputs all of the dependency information for a build
*/
protected
void logDependencies() {
if (!suppressDependencyLog && !fullDependencyList.isEmpty()) {
String[] array2 = new String[fullDependencyList.size() + 1];
array2[0] = "Depends";
int i = 1;
for (Project<?> project : fullDependencyList) {
array2[i] = project.name;
if (project.version != null) {
array2[i] += " " + project.version;
}
if (project instanceof ProjectJar) {
array2[i] += " (jar)";
}
i++;
}
BuildLog.println().println((Object[]) array2).println();
}
}
/**
* Add paths to the list of sources that are available when compiling the code
*/
public
T sourcePath(Paths sourcePaths) {
if (sourcePaths == null) {
throw new NullPointerException("Source paths cannot be null!");
}
this.sourcePaths.add(sourcePaths);
// ALWAYS add the source paths to be checksumed!
hash.add(sourcePaths);
return (T) this;
}
/**
* Add paths to the list of sources that are available when compiling the code
*/
public
T sourcePath(String srcDir) {
if (srcDir.endsWith("src")) {
String parent = new File(srcDir).getAbsoluteFile()
.getParent();
hash.add(new Paths(parent));
}
return sourcePath(new Paths(srcDir, "./"));
}
/**
* Add paths to the list of sources that are available when compiling the code
*/
public
T sourcePath(String dir, String... patterns) {
return sourcePath(new Paths(dir, patterns));
}
/**
* Add a class to the list of sources that are available when compiling the code
*/
public
T sourcePath(final Class<?> sourceClass) {
return sourcePath(Builder.getJavaFile(sourceClass));
}
/**
* requires output file to exist for build to succeed
*/
public
T depends(String projectOrJar) {
if (projectOrJar == null) {
throw new NullPointerException("Dependencies cannot be null!");
}
// sometimes it's a jar file, not a project
File file = new File(projectOrJar);
if (file.canRead()) {
ProjectJar.create(projectOrJar).outputFile(file);
}
Project<?> project = deps.get(projectOrJar);
if (project != null) {
this.dependencies.add(project);
}
else {
this.unresolvedDependencies.add(projectOrJar);
}
return (T) this;
}
/**
* requires output file to exist for build to succeed
*/
public
T depends(Project<?> project) {
if (project == null) {
throw new NullPointerException("Dependencies cannot be null!");
}
this.licenses.addAll(project.licenses);
this.dependencies.add(project);
return (T) this;
}
/**
* Adds java files, as Paths, to be built. This removes the need for building a temp jar first.
*/
public
T depends(final Paths dependencies) {
hash.add(dependencies);
sourceDependencies.add(dependencies);
return (T) this;
}
/**
* Adds java files, as Paths, to be built. This removes the need for building a temp jar first.
*/
public
T depends(final Class<?> dependencyClass) {
return depends(Builder.getJavaFile(dependencyClass));
}
/**
* @return a list of all projects that are recursive dependencies of this project. ONLY VALID AFTER A BUILD
*/
public
List<Project<?>> getFullDependencyList() {
return fullDependencyList;
}
/**
* extra files to include when you jar the project
* <p/>
* The TARGET location in the JAR, is the RELATIVE location when adding the paths. <br>
* For Example: <br>
* extraFiles(new Paths("foo", "bar/x.bmp")) <br>
* jar <br>
* - a <br>
* - b <br>
* - bar/x.bmp <br>
* <br>
* extraFiles(new Paths("foo/bar", "x.bmp")) <br>
* jar <br>
* - a <br>
* - b <br>
* - x.bmp
*/
public
T extraFiles(Paths filePaths) {
this.extraFiles.add(filePaths);
return (T) this;
}
public
T extraFiles(File file) {
Paths paths = new Paths(file.getParent(), file.getName());
Iterator<File> fileIterator = paths.fileIterator();
while (fileIterator.hasNext()) {
File next = fileIterator.next();
if (!next.canRead()) {
BuildLog.title("Error").println("Unable to read specified extra file: '" + file.getAbsolutePath() + "'");
}
}
this.extraFiles.add(paths);
return (T) this;
}
/**
* @return all of the extra files for this project
*/
public
Paths extraFiles() {
return this.extraFiles;
}
public
T outputFile(final File outputFile) {
return outputFile(outputFile.getAbsolutePath(), null);
}
public
T outputFile(final File outputFile, final File outputSourceFile) {
return outputFile(outputFile.getAbsolutePath(), outputSourceFile.getAbsolutePath());
}
public
T outputFile(final String outputFileName) {
return outputFile(outputFileName, null);
}
/**
* If the specified file is ONLY a filename, then it (and the source file, if necessary) will be placed into the staging directory.
* If a path + name is specified, then they will be placed as is.
* <p>
* If no extension is provide, the default is '.jar'
*/
public
T outputFile(String outputFileName, String outputSourceFileName) {
// this method is offset, for setting the output file via jar VS via constructor (constructor must always call outputFile0)
// if the constructor calls outputFile() instead, it will not work because of how methods are overloaded.
return outputFile0(outputFileName, outputSourceFileName);
}
private
T outputFile0(String outputFileName, String outputSourceFileName) {
// output file is used for hash checking AND for new builds
if (!outputFileName.contains(File.separator)) {
outputFileName = new File(this.stagingDir, outputFileName).getAbsolutePath();
}
if (outputSourceFileName != null && !outputSourceFileName.contains(File.separator)) {
outputSourceFileName = new File(this.stagingDir, outputSourceFileName).getAbsolutePath();
}
this.outputFile = new OutputFile(version, outputFileName, outputSourceFileName);
return (T) this;
}
public
T dist(String distLocation) {
this.distLocation = FileUtil.normalize(distLocation).getAbsolutePath();
return (T) this;
}
public
T dist(File distLocation) {
this.distLocation = FileUtil.normalize(distLocation).getAbsolutePath();
return (T) this;
}
public
T options(BuildOptions buildOptions) {
this.buildOptions = buildOptions;
return (T) this;
}
public
BuildOptions options() {
if (this.buildOptions == null) {
this.buildOptions = new BuildOptions();
}
return this.buildOptions;
}
/**
* This call needs to be (at least) before dependencies are added, otherwise the order of licenses might be in the incorrect order.
* Preferably, this should be the very first call.
*/
public
T license(License license) {
if (!calledLicenseBefore) {
calledLicenseBefore = true;
if (!this.licenses.isEmpty()) {
BuildLog.println("This is the first license added, yet there are already licenses' present. This is probably not the order of " +
"licenses that you want. We suggest calling .license() before adding dependencies.");
}
}
this.licenses.add(license);
return (T) this;
}
/**
* This call needs to be (at least) before dependencies are added, otherwise the order of licenses might be in the incorrect order.
* Preferably, this should be the very first call.
*/
public
T license(List<License> licenses) {
if (!calledLicenseBefore) {
calledLicenseBefore = true;
if (!this.licenses.isEmpty()) {
BuildLog.println("This is the first license added, yet there are already licenses' present. This is probably not the order of " +
"licenses that you want. We suggest calling .license() before adding dependencies.");
}
}
this.licenses.addAll(licenses);
return (T) this;
}
public
File getStagingDir() {
return stagingDir;
}
public
void copyFiles(String targetLocation) throws IOException {
copyFiles(FileUtil.normalize(targetLocation));
}
public
void copyFiles(File targetLocation) throws IOException {
File file = null;
if (this.outputFile != null) {
file = this.outputFile.get();
}
// copy dist dir over
boolean canCopySingles = false;
if (this.distLocation != null) {
Builder.copyDirectory(this.distLocation, targetLocation.getAbsolutePath());
if (file == null || !file.getAbsolutePath().startsWith(this.distLocation)) {
canCopySingles = true;
}
}
else {
canCopySingles = true;
}
if (canCopySingles) {
if (file != null && file.canRead()) {
Builder.copyFile(file, new File(targetLocation, file.getName()));
}
File source = null;
if (this.outputFile != null) {
source = this.outputFile.getSource();
}
// do we have a "source" file as well?
if (source != null && source.canRead()) {
Builder.copyFile(source, new File(targetLocation, source.getName()));
}
}
// now copy out extra files
List<String> fullPaths = this.extraFiles.getPaths();
List<String> relativePaths = this.extraFiles.getRelativePaths();
for (int i = 0; i < fullPaths.size(); i++) {
File source = new File(fullPaths.get(i));
if (source.isFile()) {
Builder.copyFile(source, new File(targetLocation, relativePaths.get(i)));
}
}
// now copy out dependencies
for (Project<?> project : this.dependencies) {
if (project instanceof ProjectJar) {
project.copyFiles(targetLocation);
}
}
}
public
void copyMainFiles(String targetLocation) throws IOException {
copyMainFiles(FileUtil.normalize(targetLocation));
}
@SuppressWarnings("WeakerAccess")
public
void copyMainFiles(File targetLocation) throws IOException {
if (this.outputFile != null) {
final File file = this.outputFile.get();
final File source = this.outputFile.getSource();
if (file != null && file.canRead()) {
Builder.copyFile(file, new File(targetLocation, file.getName()));
}
// do we have a "source" file as well?
if (source != null && source.canRead()) {
Builder.copyFile(source, new File(targetLocation, source.getName()));
}
}
}
public
T version(BuildVersion version) {
this.version = version;
return (T) this;
}
/**
* This cleans up and deletes the staging directory
*/
public
void cleanup() {
if (fullDependencyList != null) {
for (Project<?> project : fullDependencyList) {
project.cleanup();
}
}
if (!skippedBuild && !keepOldVersion && !(getClass().getSimpleName().equals(ProjectJar.class.getSimpleName()))) {
// before we create the jar (and sources if necessary), we delete any of the old versions that might be in the target
// directory.
if (this.version != null) {
if (this.version.hasChanged()) {
final File originalJar = this.outputFile.getOriginal();
final File originalSource = this.outputFile.getSourceOriginal();
Builder.delete(originalJar);
Builder.delete(originalSource);
}
}
else {
final File originalJar = this.outputFile.getOriginal();
final File originalSource = this.outputFile.getSourceOriginal();
if (!this.outputFile.get().equals(originalJar) &&
!this.outputFile.getSource().equals(originalSource)) {
Builder.delete(originalJar);
Builder.delete(originalSource);
}
}
}
if (stagingDir.exists()) {
BuildLog.start();
BuildLog.title("Cleanup").println("Deleting staging location: " + this.stagingDir);
FileUtil.delete(this.stagingDir);
final File[] files = this.stagingDir.listFiles();
if (files == null || files.length == 0) {
// we delete the entire staging dir, not just the one for ourselves, only if it's empty
BuildLog.println("Deleting staging location" + this.stagingDir.getParentFile());
FileUtil.delete(this.stagingDir.getParentFile());
}
BuildLog.finish();
}
}
/**
* Will keep the old files. Meaning that if you have SuperCool-v1.4, and release SuperCool-v1.5; the v1.4 release will not be deleted.
*/
public
T keepOldVersion() {
keepOldVersion = true;
return (T) this;
}
/**
* Description used for the build process
*/
public
T description(String description) {
this.description = description;
return (T) this;
}
/**
* Description used for the build process
*/
public
T description(License license) {
this.description = license.notes.get(0);
return (T) this;
}
/**
* Sets this project to be temporary, meaning that the decision to build this does NOT depend on the output files from this project
* existing, meaning that this project will ALWAYS build if the parent, non-temp project needs it to (ie: source code changes)
*/
public
T temporary() {
temporary = true;
return (T) this;
}
@Override
public
String toString() {
return this.name;
}
@Override
public
int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (this.name == null ? 0 : this.name.hashCode());
return result;
}
@Override
public
boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Project<?> other = (Project<?>) obj;
if (this.name == null) {
if (other.name != null) {
return false;
}
}
else if (!this.name.equals(other.name)) {
return false;
}
return true;
}
public
void uploadToMaven() throws IOException {
if (buildOptions.compiler.saveBuild) {
// only if we save the build. Test builds don't save, and we shouldn't upload them to maven
}
}
/**
* Saves the project details to the specified location
* @param location
*/
public abstract
void save(final String location);
/**
* Forces a particular build to always build, even if it has been built before
*/
public
void forceRebuild() {
forceRebuild = true;
}
}