blob: ba1e3b96b335697965b54b5ea7c1a95a2ef4faed [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.sis.buildtools.gradle;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.jar.Manifest;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.gradle.api.Project;
import org.gradle.api.tasks.bundling.Jar;
/**
* Extension to Gradle {@link Jar} task. This extension generates one JAR file per module.
* If the {@code build.gradle} file specifies a {@code Main-Class} attribute, the latter
* can apply to only one module, which is identified in this class as the "main module".
*
* @author Martin Desruisseaux (Geomatys)
*/
final class ModularJAR extends ZipWriter.JDK {
/**
* Version of the GeoAPI dependency.
* This value depends on the Apache SIS branch being compiled.
*
* @todo should be specified in a property.
*/
private static final String GEOAPI_VERSION = "4.0-SNAPSHOT";
/**
* Attributes where values are class names. Those attributes can be assigned
* to only one module, because the same class cannot appear in two modules.
*/
private static final String[] CLASSNAME_ATTRIBUTES = {
Attributes.Name.MAIN_CLASS.toString(),
"RegistrationClassName"
};
/**
* Creates a helper instance.
*
* @param project the sub-project being compiled.
* @param out output stream of the JAR file to create.
*/
private ModularJAR(final Project project, final JarOutputStream out) {
super(project, out);
}
/**
* Lists all compiled modules in the current Gradle sub-project.
*
* @return all modules in current Gradle sub-project.
*/
private static File[] listModules(final Project project) {
return fileRelativeToBuild(project, MAIN_CLASSES_DIRECTORY).listFiles(File::isDirectory);
}
/**
* Invoked when the {@code jar} task is executed.
*
* @param context the extension which is invoking this task.
* @param task the {@link Jar} task to configure.
*/
static void execute(final BuildHelper context, final Jar task) {
final Project project = task.getProject();
final Map<String,?> attributes = task.getManifest().getAttributes();
final var filteredAttributes = new HashMap<String,String>();
final var classnameAttributes = new HashMap<String,String>();
for (final String classname : CLASSNAME_ATTRIBUTES) {
final Object value = attributes.get(classname);
if (value instanceof String) {
final String path = ((String) value).replace('.', File.separatorChar) + ".class";
classnameAttributes.put(classname, path);
}
}
final File target = fileRelativeToBuild(project, LIBS_DIRECTORY);
for (final File module : listModules(project)) {
for (final Iterator<Map.Entry<String,String>> it = classnameAttributes.entrySet().iterator(); it.hasNext();) {
final Map.Entry<String,String> entry = it.next();
if (new File(module, entry.getValue()).isFile()) {
final String key = entry.getKey();
filteredAttributes.put(key, (String) attributes.get(key));
it.remove();
}
}
try {
final String name = module.getName();
write(project, module, new File(target, name + ".jar"), filteredAttributes);
ModularSources.write(task, name, false);
ModularSources.write(task, name, true);
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
filteredAttributes.clear();
}
for (final Map.Entry<String,String> entry : classnameAttributes.entrySet()) {
task.getLogger().warn(entry.getKey() + " not found: " + entry.getValue());
}
try {
linkDependencies(project, target);
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (UnsupportedOperationException e) {
project.getLogger().warn("Links to dependencies not provided because hard-links are not supported on this platform.", e);
}
}
/**
* Writes a JAR file for a single module.
*
* @param project the project for which to write a JAR file.
* @param source root directory of compiled class files for the module to package.
* @param target the file to write.
* @param specific attributes specific to this module.
* @throws IOException if an error occurred while reading the source or writing the JAR file.
*/
private static void write(final Project project, final File source, final File target,
final Map<String,String> specific) throws IOException
{
final var mf = new Manifest();
final Attributes attributes = mf.getMainAttributes();
attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
attributes.put(Attributes.Name.SPECIFICATION_TITLE, "GeoAPI");
attributes.put(Attributes.Name.SPECIFICATION_VENDOR, "Open Geospatial Consortium");
attributes.put(Attributes.Name.SPECIFICATION_VERSION, GEOAPI_VERSION);
attributes.put(Attributes.Name.IMPLEMENTATION_TITLE, "Apache Spatial Information System (SIS)");
attributes.put(Attributes.Name.IMPLEMENTATION_VENDOR, "The Apache Software Foundation");
attributes.put(Attributes.Name.IMPLEMENTATION_VERSION, project.getVersion().toString());
for (final Map.Entry<String,String> entry : specific.entrySet()) {
attributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
}
try (JarOutputStream out = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(target)), mf)) {
new ModularJAR(project, out).writeDirectory(source, null, "");
}
}
/**
* Adds hard-links to all dependencies.
*
* @param project the project for which to add dependencies.
* @param target where to write dependencies.
* @throws IOException if an I/O error occurred while reading the module description.
* @throws UnsupportedOperationException if links are not supported on this platform.
*/
private static void linkDependencies(final Project project, final File target) throws IOException {
for (final Dependency dep : Dependency.find(project)) {
if (dep.module != null) {
final Path f = new File(target, dep.module + ".jar").toPath();
if (Files.notExists(f)) {
Files.createLink(f, dep.file.toPath());
}
}
}
}
}