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.BuildContext;
035import org.jdrupes.builder.api.BuildException;
036import org.jdrupes.builder.api.ConfigurationException;
037import org.jdrupes.builder.api.FileResource;
038import org.jdrupes.builder.api.FileTree;
039import static org.jdrupes.builder.api.Intent.*;
040import org.jdrupes.builder.api.Launcher;
041import org.jdrupes.builder.api.Project;
042import org.jdrupes.builder.api.RootProject;
043import org.jdrupes.builder.core.AbstractRootProject;
044import org.jdrupes.builder.core.DefaultBuildContext;
045import org.jdrupes.builder.java.ClasspathElement;
046import org.jdrupes.builder.java.ClasspathScanner;
047import org.jdrupes.builder.java.JavaCompiler;
048import org.jdrupes.builder.mvnrepo.MvnRepoLookup;
049
050/// An implementation of a [Launcher] that bootstraps the build.
051/// The [BootstrapProjectLauncher] uses the built-in [BootstrapRoot] and
052/// [BootstrapBuild] to assemble a JDrupes Builder [Project] (the
053/// bootstrap project) that includes the [JavaCompiler] for compiling
054/// the JDrupes Builder configuration provided by the user. 
055/// 
056/// The launcher then requests the *supplied* and *exposed* classes from
057/// the bootstrap project, including in particular the [RootProject] of
058/// the user's build configuration. The launcher uses these classes as
059/// classpath for creating the [BuildProjectLauncher]
060///
061public class BootstrapProjectLauncher extends AbstractLauncher {
062
063    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
064    /// The JDrupes Builder properties read from the file
065    /// `.jdbld.properties` in the root project.
066    protected Properties jdbldProps;
067    /// The command line.
068    protected CommandLine commandLine;
069    private final AbstractRootProject bootstrapProject;
070    private final Path buildRootDirectory;
071
072    /// Initializes a new bootstrap launcher.
073    ///
074    /// @param rootPrjCls the root project class
075    /// @param args the arguments
076    ///
077    @SuppressWarnings("PMD.UseVarargs")
078    public BootstrapProjectLauncher(
079            Class<? extends RootProject> rootPrjCls, String[] args) {
080        buildRootDirectory = Path.of("").toAbsolutePath();
081        jdbldProps = propertiesFromFiles(buildRootDirectory);
082        try {
083            commandLine = new DefaultParser().parse(baseOptions(), args);
084        } catch (ParseException e) {
085            configureLogging(buildRootDirectory, jdbldProps);
086            throw new ConfigurationException().cause(e);
087        }
088        addCliProperties(jdbldProps, commandLine);
089        configureLogging(buildRootDirectory, jdbldProps);
090
091        bootstrapProject = DefaultBuildContext.createProjects(
092            buildRootDirectory, rootPrjCls, Collections.emptyList(), jdbldProps,
093            commandLine);
094    }
095
096    @Override
097    public void close() {
098        bootstrapProject.close();
099    }
100
101    /// Builds the build project launcher.
102    ///
103    /// @param rootPrjCls the root project
104    /// @param args the args
105    /// @return the builds the project launcher
106    ///
107    @SuppressWarnings("PMD.UseVarargs")
108    public BuildProjectLauncher buildBuildProjectLauncher(
109            Class<? extends RootProject> rootPrjCls, String[] args) {
110        return bootstrapProject.context().call(() -> {
111            URL[] cpUrls = buildProjectClasses(bootstrapProject);
112            logger.atFine().log("Build project launcher with classpath: %s",
113                Arrays.toString(cpUrls));
114            return new BuildProjectLauncher(
115                new URLClassLoader(cpUrls, getClass().getClassLoader()),
116                buildRootDirectory, args);
117        });
118    }
119
120    private URL[] buildProjectClasses(RootProject rootProject) {
121        // Add build extensions to the build project.
122        var mvnLookup = new MvnRepoLookup();
123        Optional.ofNullable(jdbldProps
124            .getProperty(BuildContext.EXTENSIONS_SNAPSHOT_REPOSITORY, null))
125            .map(URI::create).ifPresent(mvnLookup::snapshotRepository);
126        var buildCoords = Arrays.asList(jdbldProps
127            .getProperty(BuildContext.BUILD_EXTENSIONS, "").split(","))
128            .stream().map(String::trim).filter(c -> !c.isBlank()).toList();
129        logger.atFine().log("Adding build extensions: %s"
130            + " to classpath for builder project compilation", buildCoords);
131        buildCoords.forEach(mvnLookup::resolve);
132        rootProject.project(BootstrapBuild.class).dependency(Expose,
133            mvnLookup);
134        var extCp = System.getenv("JDBLD_EXTS");
135        if (extCp != null) {
136            rootProject.project(BootstrapBuild.class)
137                .dependency(Expose, ClasspathScanner::new).path(extCp);
138        }
139        return rootProject.resources(rootProject
140            .of(ClasspathElement.class).using(Supply, Expose)).map(cpe -> {
141                try {
142                    if (cpe instanceof FileTree tree) {
143                        return tree.root().toFile().toURI().toURL();
144                    }
145                    return ((FileResource) cpe).path().toFile().toURI()
146                        .toURL();
147                } catch (MalformedURLException e) {
148                    // Cannot happen
149                    throw new BuildException().from(rootProject).cause(e);
150                }
151            }).toArray(URL[]::new);
152    }
153
154    @Override
155    public AbstractRootProject rootProject() {
156        return bootstrapProject;
157    }
158
159    @Override
160    public RootProject regenerateRootProject() {
161        throw new UnsupportedOperationException(
162            "The bootstrap launcher does not support regenerate");
163    }
164
165    /// The main method.
166    ///
167    /// @param args the arguments
168    ///
169    @SuppressWarnings("PMD.SystemPrintln")
170    public static void main(String[] args) {
171        try {
172            if (!reportBuildException(() -> {
173                BuildProjectLauncher buildPl;
174                try (var bootPl = new BootstrapProjectLauncher(
175                    BootstrapRoot.class, args)) {
176                    buildPl = bootPl.buildBuildProjectLauncher(
177                        BootstrapRoot.class, args);
178                }
179                try (buildPl) {
180                    return buildPl.runCommands();
181                }
182            })) {
183                System.exit(1);
184            }
185        } catch (BuildException e) {
186            if (e.getCause() == null) {
187                logger.atSevere().log("Build failed: %s",
188                    formatter().summary(e));
189            } else {
190                logger.atSevere().withCause(e).log("Build failed: %s",
191                    formatter().summary(e));
192            }
193            System.out.println(formatter().summary(e));
194            if (!e.details().isBlank()) {
195                System.out.println(e.details());
196            }
197            System.exit(2);
198        }
199    }
200}