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 extCp = System.getenv("JDBLD_EXTS");
127        if (extCp != null) {
128            rootProject.project(BootstrapBuild.class)
129                .dependency(Expose, ClasspathScanner::new).path(extCp);
130        }
131        var mvnLookup = extensionsLookup();
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        return rootProject.resources(rootProject
141            .of(CodeContributionType).using(Supply, Expose)).map(cpe -> {
142                try {
143                    if (cpe instanceof FileTree tree) {
144                        return tree.root().toFile().toURI().toURL();
145                    }
146                    return ((FileResource) cpe).path().toFile().toURI()
147                        .toURL();
148                } catch (MalformedURLException e) {
149                    // Cannot happen
150                    throw new BuildException().from(rootProject).cause(e);
151                }
152            }).toArray(URL[]::new);
153    }
154
155    private MvnRepoLookup extensionsLookup() {
156        var mvnLookup = new MvnRepoLookup();
157        var extRepoUrls = Optional.ofNullable(jdbldProps
158            .getProperty(BuildContext.EXTENSIONS_REPOSITORIES, null))
159            .stream().flatMap(s -> Arrays.stream(s.split(",")))
160            .map(String::trim).map(URI::create).toList();
161        if (extRepoUrls.isEmpty()) {
162            mvnLookup.addRepositories(MavenContext.mavenCentral(),
163                MavenContext.jdbldDistribution());
164        } else {
165            for (int i = 0; i < extRepoUrls.size(); i++) {
166                mvnLookup.addRepository("extensionRepo_" + i,
167                    extRepoUrls.get(i), MvnVersionType.RELEASE);
168            }
169        }
170        Optional.ofNullable(jdbldProps
171            .getProperty(BuildContext.EXTENSIONS_SNAPSHOT_REPOSITORY, null))
172            .map(URI::create).ifPresent(esr -> mvnLookup.addRepository(
173                "extensionSnapshots", esr, MvnVersionType.SNAPSHOT));
174        return mvnLookup;
175    }
176
177    @Override
178    public AbstractRootProject rootProject() {
179        return bootstrapProject;
180    }
181
182    @Override
183    public RootProject regenerateRootProject() {
184        throw new UnsupportedOperationException(
185            "The bootstrap launcher does not support regenerate");
186    }
187
188    /// The main method.
189    ///
190    /// @param args the arguments
191    ///
192    @SuppressWarnings("PMD.SystemPrintln")
193    public static void main(String[] args) {
194        try {
195            if (!reportBuildException(() -> {
196                BuildProjectLauncher buildPl;
197                try (var bootPl = new BootstrapProjectLauncher(
198                    BootstrapRoot.class, args)) {
199                    buildPl = bootPl.buildBuildProjectLauncher(
200                        BootstrapRoot.class, args);
201                }
202                try (buildPl) {
203                    return buildPl.runCommands();
204                }
205            })) {
206                System.exit(1);
207            }
208        } catch (BuildException e) {
209            if (e.getCause() == null) {
210                logger.atSevere().log("Build failed: %s",
211                    formatter().summary(e));
212            } else {
213                logger.atSevere().withCause(e).log("Build failed: %s",
214                    formatter().summary(e));
215            }
216            System.out.println(formatter().summary(e));
217            if (!e.details().isBlank()) {
218                System.out.println(e.details());
219            }
220            System.exit(2);
221        }
222    }
223}