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

com.redhat.ceylon.launcher.Bootstrap Maven / Gradle / Ivy

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

import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import com.redhat.ceylon.common.Constants;

/**
 * This is the earliest bootstrap class for the Ceylon tool chain.
 * It does nothing more than trying to locate the system repository
 * and load an appropriate ceylon.bootstrap module.
 * Appropriate in this case means it will try to find the version this
 * class was compiled with (see Versions.CEYLON_VERSION_NUMBER)
 * or the version specified by the CEYLON_VERSION environment
 * variable.
 * After it locates the module it will pass the execution on to the
 * Launcher.main() it contains.
 * 
 * IMPORTANT This class should contain as little logic as possible and
 * delegate as soon as it can to the Launcher in the
 * ceylon.bootstrap module. This way we can maintain backward and forward
 * compatibility as much as possible.
 * 
 * @author Tako Schotanus
 */
public class Bootstrap {

    public static final String CEYLON_DOWNLOAD_BASE_URL = "https://downloads.ceylon-lang.org/cli/";
    
    public static final String FILE_BOOTSTRAP_PROPERTIES = "ceylon-bootstrap.properties";
    public static final String FILE_BOOTSTRAP_JAR = "ceylon-bootstrap.jar";
    
    public static final String KEY_SHA256SUM = "sha256sum";
    public static final String KEY_INSTALLATION = "installation";
    public static final String KEY_DISTRIBUTION = "distribution";

    private static final String FOLDER_DISTS = "dists";
    
    private static final int DOWNLOAD_TIMEOUT_READ = 30000;
    private static final int DOWNLOAD_TIMEOUT_CONNECT = 15000;
    private static final int DOWNLOAD_BUFFER_SIZE = 4096;

    private static final String ENV_CEYLON_BOOTSTRAP_DISTS = "CEYLON_BOOTSTRAP_DISTS";
    private static final String ENV_CEYLON_BOOTSTRAP_PROPS = "CEYLON_BOOTSTRAP_PROPERTIES";
    
    private static final String PROP_CEYLON_BOOTSTRAP_DISTS = "ceylon.bootstrap.dists";
    private static final String PROP_CEYLON_BOOTSTRAP_PROPS = "ceylon.bootstrap.properties";

    public static void main(String[] args) throws Throwable {
        // we don't need to clean up the class loader when run from main because the JVM will either exit, or
        // keep running with daemon threads in which case it will keep needing this classloader open 
        int exit = run(args);
        // WARNING: NEVER CALL EXIT IF WE STILL HAVE DAEMON THREADS RUNNING AND WE'VE NO REASON TO EXIT WITH A NON-ZERO CODE
        if (exit != 0) {
            System.exit(exit);
        }
    }

    public static int run(String... args) throws Throwable {
        CeylonClassLoader cl = null;
        try {
            Integer result = -1;
            Method runMethod = null;
            try {
                String ceylonVersion;
                if (isDistBootstrap()) {
                    // Load configuration
                    Config cfg = loadBootstrapConfig();
                    setupDistHome(cfg);
                    ceylonVersion = determineDistVersion();
                } else if (distArgument(args) != null) {
                    String dist = distArgument(args);
                    args = stripDistArgument(args);
                    Config cfg = createDistributionConfig(dist);
                    setupDistHome(cfg);
                    ceylonVersion = determineDistVersion();
                } else {
                    ceylonVersion = LauncherUtil.determineSystemVersion();
                }
                File module = CeylonClassLoader.getRepoJar("ceylon.bootstrap", ceylonVersion);
                if (!module.exists()) {
                    File homeLib = new File(System.getProperty(Constants.PROP_CEYLON_HOME_DIR), "lib");
                    module = new File(homeLib, FILE_BOOTSTRAP_JAR);
                }
                cl = CeylonClassLoader.newInstance(Arrays.asList(module));
                Class launcherClass = cl.loadClass("com.redhat.ceylon.launcher.Launcher");
                runMethod = launcherClass.getMethod("run", String[].class);
            } catch (Exception e) {
                System.err.println("Fatal: Ceylon command could not be executed");
                if (e.getCause() != null) {
                    throw e;
                } else {
                    if (!(e instanceof RuntimeException) || e.getMessage() == null) {
                        System.err.println("   --> " + e.toString());
                    } else {
                        System.err.println("   --> " + e.getMessage());
                    }
                    return -1;
                }
            }
            try {
                result = (Integer)runMethod.invoke(null, (Object)args);
            } catch (InvocationTargetException e) {
                throw e.getCause();
            }
            return result.intValue();
        } finally {
            if (cl != null) {
                cl.clearCache();
                try {
                    cl.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }
    }
    
    private static boolean isDistBootstrap() throws URISyntaxException {
        File propsFile = getPropertiesFile();
        return propsFile.exists();
    }
    
    private static String distArgument(String[] args) {
        for (String arg : args) {
            if (!arg.startsWith("-")) {
                break;
            }
            if (arg.startsWith("--distribution=") && arg.length() > 15) {
                return arg.substring(15);
            }
        }
        return null;
    }

    private static String[] stripDistArgument(String[] args) {
        ArrayList lst = new ArrayList();
        for (String arg : args) {
            if (!arg.startsWith("--distribution=") || arg.length() <= 15) {
                lst.add(arg);
            }
        }
        String[] buf = new String[lst.size()];
        return lst.toArray(buf);
    }

    private static void setupDistHome(Config cfg) throws Exception {
        // If hash doesn't exist in dists folder we must download & install
        if (!cfg.distributionDir.exists()) {
            install(cfg);
            if (!cfg.distributionDir.exists()) {
                throw new RuntimeException("Unable to install distribution");
            }
        }
        // Set the correct home folder
        System.setProperty(Constants.PROP_CEYLON_HOME_DIR, cfg.distributionDir.getAbsolutePath());
    }
    
    private static void install(Config cfg) throws Exception {
        File tmpFile = null;
        File tmpFolder = null;
        try {
            // Check if the distribution URI refers to a remote or a local file
            File zipFile;
            if (cfg.distribution.getScheme() != null) {
                // Set up a download progress monitor if we have a console
                ProgressMonitor monitor = null;
                if (System.console() != null) {
                    monitor = new ProgressMonitor() {
                        @Override
                        public void update(long read, long size) {
                            String progress;
                            if (size == -1) {
                                progress = String.valueOf(read / 1024L) + "K";
                            } else {
                                progress = String.valueOf(read * 100 / size) + "%";
                            }
                            System.out.print("Downloading Ceylon... " + progress + "\r");
                        }
                    };
                }
                // Start download of URL to temp file
                tmpFile = zipFile = File.createTempFile("ceylon-bootstrap-dist-", ".part");
                setupProxyAuthentication();
                download(cfg.distribution, zipFile, monitor);
            } else {
                // It's a local file, no need to download
                zipFile = new File(cfg.properties.getParentFile(), cfg.distribution.getPath()).getAbsoluteFile();
            }
            // Verify zip file if we have a sha sum
            if (cfg.sha256sum != null) {
                String sum = calculateSha256Sum(zipFile);
                if (!sum.equals(cfg.sha256sum)) {
                    throw new RuntimeException("Error verifying Ceylon distribution archive: SHA sums do not match");
                }
            }
            // Unzip file to temp folder in dists folder
            mkdirs(cfg.resolvedInstallation);
            tmpFolder = Files.createTempDirectory(cfg.resolvedInstallation.toPath(), "ceylon-bootstrap-dist-").toFile();
            extractArchive(zipFile, tmpFolder);
            validateDistribution(cfg, tmpFolder);
            // Rename temp folder to hash
            tmpFolder.renameTo(cfg.distributionDir);
            // Clearing the download progress text on the console
            System.out.print("                              \r");
        } finally {
            // Delete temp file and folder
            if (tmpFile != null) {
                delete(tmpFile);
            }
            if (tmpFolder != null) {
                delete(tmpFolder);
            }
        }
    }
    
    private static void validateDistribution(Config cfg, File tmpFolder) {
        File binDir = new File(tmpFolder, Constants.CEYLON_BIN_DIR);
        File libDir = new File(tmpFolder, "lib");
        File repoDir = new File(tmpFolder, "repo");
        boolean valid = binDir.exists() && libDir.exists() && repoDir.exists();
        if (!valid) {
            throw new RuntimeException("Not a valid Ceylon distribution archive: " + cfg.distribution);
        }
        File bootstrapLibJar = new File(libDir, FILE_BOOTSTRAP_JAR);
        if (!bootstrapLibJar.exists()) {
            throw new RuntimeException("Ceylon distribution archive is too old and not supported: " + cfg.distribution);
        }
    }

    private static File getPropertiesFile() throws URISyntaxException {
        String cbp;
        if ((cbp  = System.getProperty(PROP_CEYLON_BOOTSTRAP_PROPS)) != null) {
            return new File(cbp);
        } else if ((cbp  = System.getenv(ENV_CEYLON_BOOTSTRAP_PROPS)) != null) {
            return new File(cbp);
        } else {
            File jar = LauncherUtil.determineRuntimeJar();
            return new File(jar.getParentFile(), FILE_BOOTSTRAP_PROPERTIES);
        }
    }
    
    private static Properties loadBootstrapProperties() throws Exception {
        File propsFile = getPropertiesFile();
        FileInputStream fileInput = null;
        try {
            fileInput = new FileInputStream(propsFile);
            Properties properties = new Properties();
            properties.load(fileInput);
            return properties;
        } finally {
            if (fileInput != null) {
                fileInput.close();
            }
        }
    }
    
    private static class Config {
        File properties;
        URI distribution;
        File installation;
        File resolvedInstallation;
        File distributionDir;
        String hash;
        String sha256sum;
    }
    
    private static Config loadBootstrapConfig() throws Exception {
        Properties properties = loadBootstrapProperties();
        Config cfg = new Config();
        
        cfg.properties = getPropertiesFile();
        
        // Obtain dist download URL
        if (!properties.containsKey(KEY_DISTRIBUTION)) {
            throw new RuntimeException("Error in bootstrap properties file: missing 'distribution'");
        }
        cfg.distribution = new URI(properties.getProperty(KEY_DISTRIBUTION));

        // See if the distribution should be installed in some other place than the default
        if (properties.containsKey(KEY_INSTALLATION)) {
            // Get the installation path
            String installString = properties.getProperty(KEY_INSTALLATION);
            // Do some simple variable expansion
            installString = installString
                    .replaceAll("^~", System.getProperty("user.home"))
                    .replace("${user.home}", System.getProperty("user.home"))
                    .replace("${ceylon.user.dir}", getUserDir().getAbsolutePath());
            cfg.installation = new File(installString);
            cfg.resolvedInstallation = cfg.properties.getParentFile().toPath().resolve(cfg.installation.toPath()).toFile().getAbsoluteFile();
        } else {
            File distsDir;
            String distsDirStr;
            if ((distsDirStr = System.getProperty(PROP_CEYLON_BOOTSTRAP_DISTS)) != null) {
                distsDir = new File(distsDirStr);
            } else if ((distsDirStr = System.getenv(ENV_CEYLON_BOOTSTRAP_DISTS)) != null) {
                distsDir = new File(distsDirStr);
            } else {
                distsDir = new File(getUserDir(), FOLDER_DISTS);
            }
            cfg.resolvedInstallation = distsDir;
        }

        // If the properties contain a sha256sum store it for later
        cfg.sha256sum = properties.getProperty(KEY_SHA256SUM);

        return updateConfig(cfg);
    }
    
    private static Config createDistributionConfig(String dist) throws URISyntaxException {
        Config cfg = new Config();
        cfg.distribution = getDistributionUri(dist);
        return updateConfig(cfg);
    }

    private static URI getDistributionUri(String dist) throws URISyntaxException {
        URI uri = new URI(dist);
        if (uri.getScheme() != null) {
            return uri;
        } else {
            return new URI(CEYLON_DOWNLOAD_BASE_URL + "ceylon-" + dist + ".zip");
        }
    }
    
    private static Config updateConfig(Config cfg) {
        // Hash the URI, it will be our distribution's folder name
        cfg.hash = hash(cfg.distribution.toString());
        
        // Make sure resolvedInstallation points to a proper installation folder
        if (cfg.installation != null) {
            cfg.resolvedInstallation = cfg.properties.getParentFile().toPath().resolve(cfg.installation.toPath()).toFile().getAbsoluteFile();
        } else {
            cfg.resolvedInstallation = new File(getUserDir(), FOLDER_DISTS);
        }
        
        // The actual installation directory for the distribution
        cfg.distributionDir = new File(cfg.resolvedInstallation, cfg.hash);
        
        return cfg;
    }
    
    private static File mkdirs(File dir) {
        if (!dir.exists() && !dir.mkdirs()) {
            throw new RuntimeException("Unable to create destination directory: " + dir);
        }
        return dir;
    }
    
    private static void delete(File f) {
        if (!delete_(f)) {
            // As a last resort
            f.deleteOnExit();
        }
    }
    
    private static boolean delete_(File f) {
        boolean ok = true;
        if (f.exists()) {
            if (f.isDirectory()) {
                for (File c : f.listFiles()) {
                    ok = ok && delete_(c);
                }
            }
            try {
                boolean deleted = f.delete();
                ok = ok && deleted;
            } catch (Exception ex) {
                ok = false;
            }
        }
        return ok;
    }
    
    private static File getDefaultUserDir() {
        String userHome = System.getProperty("user.home");
        return new File(userHome, ".ceylon");
    }

    private static File getUserDir() {
        String ceylonUserDir = System.getProperty(Constants.PROP_CEYLON_USER_DIR);
        if (ceylonUserDir != null) {
            return new File(ceylonUserDir);
        } else {
            return getDefaultUserDir();
        }
    }
    
    private static void extractArchive(File zip, File dir) throws IOException {
        if (dir.exists()) {
            if (!dir.isDirectory()) {
                throw new RuntimeException("Error extracting archive: destination not a directory: " + dir);
            }
        } else {
            mkdirs(dir);
        }

        ZipFile zf = null;
        try {
            zf = new ZipFile(zip);
            Enumeration entries = zf.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                String entryName = stripRoot(entry.getName());
                try {
                    if (entryName.isEmpty()) {
                        continue;
                    }
                    File out = new File(dir, entryName);
                    if (entry.isDirectory()) {
                        mkdirs(out);
                        continue;
                    }
                    mkdirs(out.getParentFile());
                    InputStream zipIn = null;
                    try {
                        zipIn = zf.getInputStream(entry);
                        BufferedOutputStream fileOut = null;
                        try {
                            fileOut = new BufferedOutputStream(new FileOutputStream(out));
                            copyStream(zipIn, fileOut, false, false);
                        } finally {
                            fileOut.close();
                        }
                    } finally {
                        zipIn.close();
                    }
                } catch (IOException e) {
                    throw new RuntimeException("Error extracting archive", e);
                }
            }
        } finally {
            zf.close();
        }
    }
    
    private static String stripRoot(String name) {
        int p = name.indexOf('/');
        if (p > 0) {
            name = name.substring(p + 1);
        }
        return name;
    }

    private static void copyStream(InputStream in, OutputStream out, boolean closeIn, boolean closeOut) throws IOException {
        try {
            copyStreamNoClose(in, out);
        } finally {
            if (closeIn) {
                safeClose(in);
            }
            if (closeOut) {
                safeClose(out);
            }
        }
    }

    private static void copyStreamNoClose(InputStream in, OutputStream out) throws IOException {
        final byte[] bytes = new byte[8192];
        int cnt;
        while ((cnt = in.read(bytes)) != -1) {
            out.write(bytes, 0, cnt);
        }
        out.flush();
    }
    
    private static void safeClose(Closeable c) {
        try {
            if (c != null) {
                c.close();
            }
        } catch (IOException ignored) {
        }
    }
    
    /**
     * This method computes a hash of the provided {@code string}.
     * Copied from Gradle's PathAssembler
     */
    private static String hash(String string) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] bytes = string.getBytes();
            messageDigest.update(bytes);
            return new BigInteger(1, messageDigest.digest()).toString(36);
        } catch (Exception e) {
            throw new RuntimeException("Error creating hash", e);
        }
    }
    
    /**
     * This method calculates the SHA256 sum of the provided {@code file}
     * Copied from Gradle's Install
     */
    private static String calculateSha256Sum(File file) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        InputStream fis = null;
        try {
            fis = new FileInputStream(file);
            int n = 0;
            byte[] buffer = new byte[4096];
            while (n != -1) {
                n = fis.read(buffer);
                if (n > 0) {
                    md.update(buffer, 0, n);
                }
            }
            byte byteData[] = md.digest();
    
            StringBuffer hexString = new StringBuffer();
            for (int i=0; i < byteData.length; i++) {
                String hex=Integer.toHexString(0xff & byteData[i]);
                if (hex.length() == 1) {
                    hexString.append('0');
                }
                hexString.append(hex);
            }
    
            return hexString.toString();
        } finally {
            fis.close();
        }
    }
    
    private static interface ProgressMonitor {
        void update(long read, long size);
    }
    
    private static void download(URI uri, File file, ProgressMonitor progress) throws IOException {
        URLConnection connection = null;
        InputStream input = null;
        OutputStream output = null;
        try {
            URL url = uri.toURL();
            connection = url.openConnection();
            connection.setConnectTimeout(DOWNLOAD_TIMEOUT_CONNECT);
            connection.setReadTimeout(DOWNLOAD_TIMEOUT_READ);
            input = connection.getInputStream();
            output = new FileOutputStream(file);
            int n;
            long read = 0;
            long size = connection.getContentLength();
            byte[] buffer = new byte[DOWNLOAD_BUFFER_SIZE];
            while ((n = input.read(buffer)) != -1) {
                output.write(buffer, 0, n);
                read += n;
                if (progress != null) {
                    progress.update(read, size);
                }
            }
        } finally {
            if (output != null) {
                output.close();
            }
            if (input != null) {
                input.close();
            }
        }
    }
    
    /**
     * Sets up proxy authentication if the associated system properties
     * are available: "http.proxyUser" and "http.proxyPassword"
     * Copied from Gradle's Download
     */
    private static void setupProxyAuthentication() {
        if (System.getProperty("http.proxyUser") != null) {
            Authenticator.setDefault(new ProxyAuthenticator());
        }
    }
    
    private static class ProxyAuthenticator extends Authenticator {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication(
                    System.getProperty("http.proxyUser"),
                    System.getProperty("http.proxyPassword", "").toCharArray());
        }
    }

    private static File determineDistLanguage(File distHome) {
        File distRepo = new File(distHome, "repo");
        File bootstrap = new File(new File(distRepo, "ceylon"), "language");
        File[] versions = bootstrap.listFiles(new FileFilter() {
            @Override
            public boolean accept(File f) {
                return f.isDirectory();
            }
        });
        if (versions == null || versions.length != 1) {
            return null;
        }
        return versions[0];
    }

    private static String determineDistVersion() {
        File distHome = new File(System.getProperty(Constants.PROP_CEYLON_HOME_DIR));
        File versionDir = determineDistLanguage(distHome);
        if (versionDir == null) {
            throw new RuntimeException("Error in distribution: missing bootstrap in " + distHome.getAbsolutePath());
        }
        return versionDir.getName();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy