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.ScopedValueContext;
045import org.jdrupes.builder.java.ClasspathScanner;
046import org.jdrupes.builder.java.JavaCompiler;
047import static org.jdrupes.builder.java.JavaTypes.*;
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 = 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 ScopedValueContext.snapshot()
111            .where(bootstrapProject.context()::startRequestChain)
112            .where(scopedBuildContext, bootstrapProject.context()).call(() -> {
113                URL[] cpUrls = buildProjectClasses(bootstrapProject);
114                logger.atFine().log("Build project launcher with classpath: %s",
115                    Arrays.toString(cpUrls));
116                return new BuildProjectLauncher(
117                    new URLClassLoader(cpUrls, getClass().getClassLoader()),
118                    buildRootDirectory, args);
119            });
120    }
121
122    private URL[] buildProjectClasses(RootProject rootProject) {
123        // Add build extensions to the build project.
124        var mvnLookup = new MvnRepoLookup();
125        Optional.ofNullable(jdbldProps
126            .getProperty(BuildContext.EXTENSIONS_SNAPSHOT_REPOSITORY, null))
127            .map(URI::create).ifPresent(mvnLookup::snapshotRepository);
128        var buildCoords = Arrays.asList(jdbldProps
129            .getProperty(BuildContext.BUILD_EXTENSIONS, "").split(","))
130            .stream().map(String::trim).filter(c -> !c.isBlank()).toList();
131        logger.atFine().log("Adding build extensions: %s"
132            + " to classpath for builder project compilation", buildCoords);
133        buildCoords.forEach(mvnLookup::resolve);
134        rootProject.project(BootstrapBuild.class).dependency(Expose,
135            mvnLookup);
136        var extCp = System.getenv("JDBLD_EXTS");
137        if (extCp != null) {
138            rootProject.project(BootstrapBuild.class)
139                .dependency(Expose, ClasspathScanner::new).path(extCp);
140        }
141        return rootProject.resources(rootProject
142            .of(ClasspathElementType).using(Supply, Expose)).map(cpe -> {
143                try {
144                    if (cpe instanceof FileTree tree) {
145                        return tree.root().toFile().toURI().toURL();
146                    }
147                    return ((FileResource) cpe).path().toFile().toURI()
148                        .toURL();
149                } catch (MalformedURLException e) {
150                    // Cannot happen
151                    throw new BuildException().from(rootProject).cause(e);
152                }
153            }).toArray(URL[]::new);
154    }
155
156    @Override
157    public AbstractRootProject rootProject() {
158        return bootstrapProject;
159    }
160
161    @Override
162    public RootProject regenerateRootProject() {
163        throw new UnsupportedOperationException(
164            "The bootstrap launcher does not support regenerate");
165    }
166
167    /// The main method.
168    ///
169    /// @param args the arguments
170    ///
171    @SuppressWarnings("PMD.SystemPrintln")
172    public static void main(String[] args) {
173        try {
174            if (!reportBuildException(() -> {
175                BuildProjectLauncher buildPl;
176                try (var bootPl = new BootstrapProjectLauncher(
177                    BootstrapRoot.class, args)) {
178                    buildPl = bootPl.buildBuildProjectLauncher(
179                        BootstrapRoot.class, args);
180                }
181                try (buildPl) {
182                    return buildPl.runCommands();
183                }
184            })) {
185                System.exit(1);
186            }
187        } catch (BuildException e) {
188            if (e.getCause() == null) {
189                logger.atSevere().log("Build failed: %s",
190                    formatter().summary(e));
191            } else {
192                logger.atSevere().withCause(e).log("Build failed: %s",
193                    formatter().summary(e));
194            }
195            System.out.println(formatter().summary(e));
196            if (!e.details().isBlank()) {
197                System.out.println(e.details());
198            }
199            System.exit(2);
200        }
201    }
202}