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