All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.redhat.ceylon.tools.assemble.CeylonAssembleTool Maven / Gradle / Ivy

There is a newer version: 1.3.3
Show newest version
package com.redhat.ceylon.tools.assemble;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import com.redhat.ceylon.cmr.api.ArtifactContext;
import com.redhat.ceylon.cmr.api.ModuleQuery;
import com.redhat.ceylon.cmr.ceylon.loader.ModuleGraph;
import com.redhat.ceylon.cmr.impl.IOUtils;
import com.redhat.ceylon.common.BooleanUtil;
import com.redhat.ceylon.common.Constants;
import com.redhat.ceylon.common.FileUtil;
import com.redhat.ceylon.common.JVMModuleUtil;
import com.redhat.ceylon.common.ModuleSpec;
import com.redhat.ceylon.common.ModuleUtil;
import com.redhat.ceylon.common.Versions;
import com.redhat.ceylon.common.config.DefaultToolOptions;
import com.redhat.ceylon.common.tool.Argument;
import com.redhat.ceylon.common.tool.Description;
import com.redhat.ceylon.common.tool.Option;
import com.redhat.ceylon.common.tool.OptionArgument;
import com.redhat.ceylon.common.tool.Summary;
import com.redhat.ceylon.common.tool.ToolUsageError;
import com.redhat.ceylon.common.tools.CeylonTool;
import com.redhat.ceylon.model.cmr.ArtifactResult;
import com.redhat.ceylon.tools.moduleloading.ModuleLoadingTool;

/**
 * @author Tako Schotanus ([email protected])
 */
@Summary("Generate a Ceylon assembly for a given module")
@Description("Generate an executable _assembly_ which contains the given module and all its run-time"
        + " dependencies, including the Ceylon run-time, which makes that jar self-sufficient and"
        + " executable by `java` as if the Ceylon module was run by `ceylon run`."
)
public class CeylonAssembleTool extends ModuleLoadingTool {
    private List modules;
    private boolean force;
	private File out;
    private final List excludedModules = new ArrayList<>();
    /** The (Ceylon) name of the functional to run, e.g. {@code foo.bar::baz} */
    private String run;
    private boolean includeLanguage;
    private boolean includeRuntime;
    private Boolean jvm;
    private Boolean js;
    private Boolean dart;
    
    private String[] assemblySuffixes;
    private ModuleQuery.Type mqt;
    
    public static final String CEYLON_ASSEMBLY_SUFFIX = ".cas";

    @Argument(order = 1, argumentName="module", multiplicity="+")
    public void setModules(List modules) {
        setModuleSpecs(ModuleSpec.parseEachList(modules));
    }

    public void setModuleSpecs(List modules) {
        this.modules = modules;
    }

    @OptionArgument(longName = "run", argumentName = "toplevel")
    @Description("Specifies the fully qualified name of a toplevel method or class with no parameters. " +
            "The format is: `qualified.package.name::classOrMethodName` with `::` acting as separator " +
            "between the package name and the toplevel class or method name (defaults to `{module}::run`).")
    public void setRun(String run) {
        this.run = run;
    }

    @Description("Target assembly file (defaults to `{name}-{version}.jar`).")
    @OptionArgument(shortName = 'o', argumentName="file")
    public void setOut(File out) {
        this.out = out;
    }

    @OptionArgument(argumentName="moduleOrFile", shortName='x')
    @Description("Excludes modules from the resulting far jat. Can be a module name or " + 
            "a file containing module names. Can be specified multiple times. Note that "+
            "this excludes the module from the resulting assembly, but if your modules require that "+
            "module to be present at runtime it will still be required and may cause your "+
            "application to fail to start if it is not provided at runtime.")
    public void setExcludeModule(List exclusions) {
        for (String each : exclusions) {
            File xFile = new File(each);
            if (xFile.exists() && xFile.isFile()) {
                try (BufferedReader reader = new BufferedReader(new FileReader(xFile))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        this.excludedModules.add(line);
                    }
                } catch (IOException e) {
                    throw new ToolUsageError(CeylonAssembleMessages.msg("exclude.file.failure", each), 
                            e);
                }
            } else {
                this.excludedModules.add(each);
            }
        }
    }

    @Option(longName="force")
    @Description("Ignore errors about conflicting modules.")
    public void setForce(boolean force) {
        this.force = force;
    }

    @Option
    @Description("Include the language module and its dependencies in the assembly."
            + " to ensures that the assembly can be run stand-alone using Java without"
            + " having Ceylon installed. This option does NOT support module isolation!")
    public void setIncludeLanguage(boolean includeLanguage) {
        this.includeLanguage = includeLanguage;
    }

    @Option
    @Description("Include enough of the Ceylon runtime in the assembly to ensure"
            + " that the assembly can be run stand-alone using Java without having"
            + " Ceylon installed. This option supports full module isolation.")
    public void setIncludeRuntime(boolean includeRuntime) {
        this.includeRuntime = includeRuntime;
    }

    @Option
    @Description("Include artifacts compiled for the JVM (`.car` and `.jar`) (default: `true`)")
    public void setJvm(boolean jvm) {
        this.jvm = jvm;
    }

    @Option
    @Description("Include artifacts compiled for JavaScript (`.js` and `-model.js`) (default: `false`)")
    public void setJs(boolean js) {
        this.js = js;
    }

    @Option
    @Description("Include artifacts compiled for Dart (`.dart` and `-dartmodel.json`) (default: `true`)")
    public void setDart(boolean dart) {
        this.dart = dart;
    }

    @Override
    protected String[] getLoaderSuffixes() {
        return assemblySuffixes;
    }
    
    @Override
    public void initialize(CeylonTool mainTool) throws Exception {
        // Determine the artifacts we'll include in the assembly
        mqt = ModuleQuery.Type.JVM;
        ArrayList sfx = new ArrayList();
        boolean defaults = js == null 
                && jvm == null
                && dart == null;
        if (BooleanUtil.isTrue(dart) || defaults) {
            sfx.add(ArtifactContext.DART);
            sfx.add(ArtifactContext.DART_MODEL);
            sfx.add(ArtifactContext.RESOURCES);
            mqt = ModuleQuery.Type.DART;
        }
        if (BooleanUtil.isTrue(js) || defaults) {
            sfx.add(ArtifactContext.JS);
            sfx.add(ArtifactContext.JS_MODEL);
            sfx.add(ArtifactContext.RESOURCES);
            mqt = ModuleQuery.Type.JS;
        }
        if (BooleanUtil.isTrue(jvm) || defaults) {
            // put the CAR first since its presence will shortcut the other three
            sfx.add(ArtifactContext.CAR);
            sfx.add(ArtifactContext.JAR);
            sfx.add(ArtifactContext.MODULE_PROPERTIES);
            sfx.add(ArtifactContext.MODULE_XML);
            mqt = ModuleQuery.Type.JVM;
        }
        assemblySuffixes = sfx.toArray(new String[] {});
        
        super.initialize(mainTool);
    }

    @Override
    public void run() throws Exception {
        if (includeRuntime) {
            // includeRuntime implies includeLanguage
            includeLanguage = true;
        }
        
        String firstModuleName = null, firstModuleVersion = null;
        for (ModuleSpec module : modules) {
            String moduleName = module.getName();
            String version = checkModuleVersionsOrShowSuggestions(
                    moduleName,
                    module.isVersioned() ? module.getVersion() : null,
                    mqt,
                    Versions.JVM_BINARY_MAJOR_VERSION,
                    Versions.JVM_BINARY_MINOR_VERSION,
                    Versions.JS_BINARY_MAJOR_VERSION,
                    Versions.JS_BINARY_MINOR_VERSION,
                    null);
            if(version == null)
                return;
            if(firstModuleName == null){
                firstModuleName = moduleName;
                firstModuleVersion = version;
            }
            loadModule(null, moduleName, version);
            if(!force)
                errorOnConflictingModule(moduleName, version);
        }
        if (includeRuntime) {
            loadModule(null, "ceylon.runtime", Versions.CEYLON_VERSION_NUMBER);
        }
        loader.resolve();
        String versionSuffix = firstModuleVersion != null && !firstModuleVersion.isEmpty()
                ? "-"+firstModuleVersion
                : "";
        File outputCas = applyCwd(out != null ? out : new File(firstModuleName + versionSuffix + CEYLON_ASSEMBLY_SUFFIX));
        if(outputCas.getParentFile() != null && !outputCas.getParentFile().exists()){
            FileUtil.mkdirs(outputCas.getParentFile());
        }
        if(outputCas.exists()){
            FileUtil.delete(outputCas);
        }
        final Set added = new HashSet<>();

        // Create a MANIFEST.MF and add it to the assembly
        Manifest manifest = new Manifest();
        Attributes mainAttributes = manifest.getMainAttributes();
        mainAttributes.putValue("Manifest-Version", "1.0");
        mainAttributes.putValue("Created-By", "Ceylon assemble for module "+firstModuleName+"/"+firstModuleVersion);
        mainAttributes.putValue(Constants.ATTR_ASSEMBLY_MAIN_MODULE, ModuleUtil.makeModuleName(firstModuleName, firstModuleVersion));
        if (run != null) {
            mainAttributes.putValue(Constants.ATTR_ASSEMBLY_RUN, run);
        }
        mainAttributes.putValue(Constants.ATTR_ASSEMBLY_REPOSITORY, "modules");
        File ovrFile = getOverridesFile();
        if (ovrFile != null) {
            mainAttributes.putValue(Constants.ATTR_ASSEMBLY_OVERRIDES, ovrFile.getName());
        }
        if (includeLanguage) {
            String className = JVMModuleUtil.javaClassNameFromCeylon(firstModuleName, run != null ? run : (firstModuleName + "::run"));
            mainAttributes.putValue("Main-Class", "com.redhat.ceylon.tools.assemble.CeylonAssemblyRunner");
            mainAttributes.putValue(Constants.ATTR_ASSEMBLY_MAIN_CLASS, className);
        }
        added.add("META-INF/");
        added.add("META-INF/MANIFEST.MF");

        try(ZipOutputStream zipFile = new JarOutputStream(new FileOutputStream(outputCas), manifest)){
            if (ovrFile != null) {
                // Copy the overrides.xml file to the output CAS
                try (InputStream is = new FileInputStream(ovrFile)) {
                    zipFile.putNextEntry(new ZipEntry(ovrFile.getName()));
                    IOUtils.copyStream(is, zipFile, true, false);
                }
            }
            
            if (includeLanguage) {
                // Copy the CeylonAssemblyRunner class and dependencies to the output CAS
                String prefix = CeylonAssemblyRunner.class.getName().replace('.', '/');
                String[] postfixes = {
                        "",
                        "$CeylonAssemblyClassLoader",
                        "$CeylonAssemblyClassLoader$1"
                };
                for (String postfix : postfixes) {
                    String clsName = prefix + postfix + ".class";
                    try (InputStream is = CeylonAssemblyRunner.class.getResourceAsStream("/" + clsName)) {
                        zipFile.putNextEntry(new ZipEntry(clsName));
                        IOUtils.copyStream(is, zipFile, true, false);
                    }
                }
            }
            
            // Visit the module and all its dependencies
            loader.visitModules(new ModuleGraph.Visitor() {
                @Override
                public void visit(ModuleGraph.Module module) {
                    if(module.artifact != null){
                        File file = module.artifact.artifact();
                        try{
                            if(file != null){
                                if(isVerbose()){
                                    append(file.getAbsolutePath());
                                    newline();
                                }
                                if (module.artifact.namespace() == null) {
                                    // Copy all the "interesting" artifacts to the assembly, not just the JVM one
                                    ArtifactContext ac = new ArtifactContext(module.artifact.namespace(), module.artifact.name(), module.artifact.version(), assemblySuffixes);
                                    List artifacts = getRepositoryManager().getArtifactResults(ac);
                                    for (ArtifactResult ar : artifacts) {
                                        String name = "modules/" + moduleToPath(module.name) + "/" + module.version + "/" + ar.artifact().getName();
                                        addEntry(zipFile, ar.artifact(), name);
                                    }
                                } else if (module.artifact.namespace().equals("maven")) {
                                    String name = "maven/" + moduleToPath(module.name) + "/" + module.version + "/" + file.getName();
                                    addEntry(zipFile, file, name);
                                    // Copy the Maven artifact's pom file as well
                                    String pomName = module.artifact.artifactId() + "-" + module.version + ".pom";
                                    File mfile = new File(file.getParentFile(), pomName);
                                    if (mfile.isFile()) {
                                        name = "maven/" + moduleToPath(module.name) + "/" + module.version + "/" + mfile.getName();
                                        addEntry(zipFile, mfile, name);
                                    }
                                } else if (module.artifact.namespace().equals("npm")) {
                                    File parent = module.artifact.artifact().getParentFile().getCanonicalFile();
                                    while (parent != null && !(new File(parent, "package.json")).exists()) {
                                        parent = parent.getParentFile();
                                    }
                                    String name = "node_modules/" + parent.getName();
                                    addEntry(zipFile, parent, name);
                                }
                            }
                        }catch(IOException x){
                            // lame
                            throw new RuntimeException(x);
                        }
                    }
                }

                private String moduleToPath(String name) {
                    return ModuleUtil.moduleToPath(name).getPath().replace(':', File.separatorChar);
                }

                private void addEntry(final ZipOutputStream zipFile, final File file, final String name) throws IOException {
                    if (file.isFile()) {
                        addFileEntry(zipFile, file, name);
                    } else if (file.isDirectory()) {
                        Files.walkFileTree(file.toPath(), new SimpleFileVisitor() {
                            @Override
                            public FileVisitResult visitFile(Path path,
                                    BasicFileAttributes attrs)
                                    throws IOException {
                                if (path.toFile().isFile()) {
                                    Path relPath = file.toPath().relativize(path);
                                    String newName = name + "/" + relPath;
                                    addFileEntry(zipFile, path.toFile(), newName);
                                }
                                return super.visitFile(path, attrs);
                            }
                        });
                    }
                }

                private void addFileEntry(ZipOutputStream zipFile, File file, String name) throws IOException {
                    try (InputStream is = new FileInputStream(file)) {
                        zipFile.putNextEntry(new ZipEntry(name));
                        IOUtils.copyStream(is, zipFile, true, false);
                    }
                }
            });
            zipFile.flush();
        }
        flush();
    }

    @Override
    protected boolean shouldExclude(String moduleName, String version) {
        return super.shouldExclude(moduleName, version) ||
                this.excludedModules.contains(moduleName) ||
                (!includeLanguage && "ceylon.language".equals(moduleName));
    }
    
    private File getOverridesFile() {
        String path = (overrides != null) ? overrides : DefaultToolOptions.getDefaultOverrides();
        if (path != null) {
            return applyCwd(new File(path));
        }
        return null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy