001/*
002 * JDrupes Builder
003 * Copyright (C) 2025, 2026 Michael N. Lipp
004 * 
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.builder.startup;
020
021import com.google.common.flogger.FluentLogger;
022import java.net.MalformedURLException;
023import java.net.URI;
024import java.net.URL;
025import java.net.URLClassLoader;
026import java.nio.file.Path;
027import java.util.Arrays;
028import java.util.Collections;
029import java.util.Optional;
030import java.util.Properties;
031import org.apache.commons.cli.CommandLine;
032import org.apache.commons.cli.DefaultParser;
033import org.apache.commons.cli.ParseException;
034import org.jdrupes.builder.api.BuildException;
035import org.jdrupes.builder.api.FileResource;
036import org.jdrupes.builder.api.FileTree;
037import static org.jdrupes.builder.api.Intent.*;
038import org.jdrupes.builder.api.Launcher;
039import org.jdrupes.builder.api.Project;
040import org.jdrupes.builder.api.RootProject;
041import org.jdrupes.builder.core.LauncherSupport;
042import org.jdrupes.builder.java.ClasspathElement;
043import org.jdrupes.builder.java.JavaCompiler;
044import org.jdrupes.builder.mvnrepo.MvnRepoLookup;
045
046/// An implementation of a [Launcher] that bootstraps the build.
047/// The [BootstrapProjectLauncher] uses the built-in [BootstrapRoot] and
048/// [BootstrapBuild] to assemble a JDrupes Builder [Project] (the
049/// bootstrap project) that includes the [JavaCompiler] for compiling
050/// the JDrupes Builder configuration provided by the user. 
051/// 
052/// The launcher then requests the *supplied* and *exposed* classes from
053/// the bootstrap project, including in particular the [RootProject] of
054/// the user's build configuration. The launcher uses these classes as
055/// classpath for creating the [BuildProjectLauncher]
056///
057public class BootstrapProjectLauncher extends AbstractLauncher {
058
059    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
060    /// The JDrupes Builder properties read from the file
061    /// `.jdbld.properties` in the root project.
062    protected Properties jdbldProps;
063    /// The command line.
064    protected CommandLine commandLine;
065    private RootProject rootProject;
066
067    /// Initializes a new bootstrap launcher.
068    ///
069    public BootstrapProjectLauncher() {
070        // Make javadoc happy.
071    }
072
073    /// Executes a build. An instance of the class passed as argument is
074    /// created and used as root project for the build.
075    /// 
076    /// Unless the root project is the only project, the root project
077    /// must declare dependencies, else the subprojects won't be
078    /// instantiated.
079    ///
080    /// @param rootPrjCls the root project
081    /// @param args the args
082    /// @return the builds the project launcher
083    ///
084    @SuppressWarnings("PMD.UseVarargs")
085    public BuildProjectLauncher buildBuildProjectLauncher(
086            Class<? extends RootProject> rootPrjCls, String[] args) {
087        Path buildRootDirectory = Path.of("").toAbsolutePath();
088        jdbldProps = propertiesFromFiles(buildRootDirectory);
089        try {
090            commandLine = new DefaultParser().parse(baseOptions(), args);
091        } catch (ParseException e) {
092            configureLogging(buildRootDirectory, jdbldProps);
093            throw new BuildException().cause(e);
094        }
095        addCliProperties(jdbldProps, commandLine);
096        configureLogging(buildRootDirectory, jdbldProps);
097
098        rootProject = LauncherSupport.createProjects(buildRootDirectory,
099            rootPrjCls, Collections.emptyList(), jdbldProps, commandLine);
100
101        // Add build extensions to the build project.
102        var mvnLookup = new MvnRepoLookup();
103        Optional.ofNullable(jdbldProps
104            .getProperty(BootstrapBuild.EXTENSIONS_SNAPSHOT_REPO, null))
105            .map(URI::create).ifPresent(mvnLookup::snapshotRepository);
106        var buildCoords = Arrays.asList(jdbldProps
107            .getProperty(BootstrapBuild.BUILD_EXTENSIONS, "").split(","))
108            .stream().map(String::trim).filter(c -> !c.isBlank()).toList();
109        logger.atFine().log("Adding build extensions: %s"
110            + " to classpath for builder project compilation", buildCoords);
111        buildCoords.forEach(mvnLookup::resolve);
112        rootProject.project(BootstrapBuild.class).dependency(Expose,
113            mvnLookup);
114        var cpUrls = rootProject.resources(rootProject
115            .of(ClasspathElement.class).using(Supply, Expose)).map(cpe -> {
116                try {
117                    if (cpe instanceof FileTree tree) {
118                        return tree.root().toFile().toURI().toURL();
119                    }
120                    return ((FileResource) cpe).path().toFile().toURI()
121                        .toURL();
122                } catch (MalformedURLException e) {
123                    // Cannot happen
124                    throw new BuildException().from(rootProject).cause(e);
125                }
126            }).toArray(URL[]::new);
127        logger.atFine().log("Launching build project with classpath: %s",
128            Arrays.toString(cpUrls));
129        return new BuildProjectLauncher(
130            new URLClassLoader(cpUrls, getClass().getClassLoader()),
131            buildRootDirectory, args);
132    }
133
134    /// Root project.
135    ///
136    /// @return the root project
137    ///
138    @Override
139    public RootProject rootProject() {
140        return rootProject;
141    }
142
143    /// The main method.
144    ///
145    /// @param args the arguments
146    ///
147    @SuppressWarnings("PMD.SystemPrintln")
148    public static void main(String[] args) {
149        try {
150            if (!reportBuildException(
151                () -> new BootstrapProjectLauncher().buildBuildProjectLauncher(
152                    BootstrapRoot.class, args).runCommands())) {
153                Runtime.getRuntime().exit(1);
154            }
155        } catch (BuildException e) {
156            if (e.getCause() == null) {
157                logger.atSevere().log("Build failed: %s",
158                    formatter().summary(e));
159            } else {
160                logger.atSevere().withCause(e).log("Build failed: %s",
161                    formatter().summary(e));
162            }
163            System.out.println(formatter().summary(e));
164            Runtime.getRuntime().exit(2);
165        }
166    }
167}