All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.realityforge.bazel.depgen.Main Maven / Gradle / Ivy

Go to download

bazel-depgen: Generate Bazel dependency scripts by traversing Maven repositories

There is a newer version: 0.19
Show newest version
package org.realityforge.bazel.depgen;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.building.SettingsBuildingException;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.graph.DependencyCycle;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.resolution.DependencyResolutionException;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.util.artifact.SubArtifact;
import org.realityforge.bazel.depgen.config.ApplicationConfig;
import org.realityforge.bazel.depgen.model.ApplicationModel;
import org.realityforge.bazel.depgen.model.InvalidModelException;
import org.realityforge.bazel.depgen.record.ApplicationRecord;
import org.realityforge.bazel.depgen.record.ArtifactRecord;
import org.realityforge.bazel.depgen.util.ArtifactUtil;
import org.realityforge.bazel.depgen.util.BazelUtil;
import org.realityforge.bazel.depgen.util.YamlUtil;
import org.realityforge.getopt4j.CLArgsParser;
import org.realityforge.getopt4j.CLOption;
import org.realityforge.getopt4j.CLOptionDescriptor;
import org.realityforge.getopt4j.CLUtil;

/**
 * The entry point in which to run the tool.
 */
public class Main
{
  private static final int VERSION_OPT = 2;
  private static final int HELP_OPT = 'h';
  private static final int QUIET_OPT = 'q';
  private static final int VERBOSE_OPT = 'v';
  private static final int RESET_CACHED_METADATA_OPT = 1;
  private static final int RUN_DIR_OPT = 'd';
  private static final int CACHE_DIR_OPT = 'r';
  private static final int SETTINGS_FILE_OPT = 's';
  private static final int CONFIG_FILE_OPT = 'c';
  private static final CLOptionDescriptor[] OPTIONS = new CLOptionDescriptor[]
    {
      new CLOptionDescriptor( "version",
                              CLOptionDescriptor.ARGUMENT_DISALLOWED,
                              VERSION_OPT,
                              "print the version and exit" ),
      new CLOptionDescriptor( "help",
                              CLOptionDescriptor.ARGUMENT_DISALLOWED,
                              HELP_OPT,
                              "print this message and exit" ),
      new CLOptionDescriptor( "quiet",
                              CLOptionDescriptor.ARGUMENT_DISALLOWED,
                              QUIET_OPT,
                              "Do not output unless an error occurs.",
                              new int[]{ VERBOSE_OPT } ),
      new CLOptionDescriptor( "verbose",
                              CLOptionDescriptor.ARGUMENT_DISALLOWED,
                              VERBOSE_OPT,
                              "Verbose output of differences.",
                              new int[]{ QUIET_OPT } ),
      new CLOptionDescriptor( "directory",
                              CLOptionDescriptor.ARGUMENT_REQUIRED,
                              RUN_DIR_OPT,
                              "The directory to run the tool from." ),
      new CLOptionDescriptor( "config-file",
                              CLOptionDescriptor.ARGUMENT_REQUIRED,
                              CONFIG_FILE_OPT,
                              "The path to the yaml file containing the dependency configuration. Defaults" +
                              " to '" + ApplicationConfig.DEFAULT_MODULE + "/" + ApplicationConfig.FILENAME + "'." ),
      new CLOptionDescriptor( "settings-file",
                              CLOptionDescriptor.ARGUMENT_REQUIRED,
                              SETTINGS_FILE_OPT,
                              "The path to the settings.xml used by Maven to extract repository credentials. " +
                              "Defaults to '~/.m2/settings.xml'." ),
      new CLOptionDescriptor( "cache-directory",
                              CLOptionDescriptor.ARGUMENT_REQUIRED,
                              CACHE_DIR_OPT,
                              "The path to the directory in which to cache downloads from remote " +
                              "repositories. Defaults to \"$(bazel info output_base)/.depgen-cache\"." ),
      new CLOptionDescriptor( "reset-cached-metadata",
                              CLOptionDescriptor.ARGUMENT_DISALLOWED,
                              RESET_CACHED_METADATA_OPT,
                              "Recalculate metadata about an artifact." )
    };
  static final String GENERATE_COMMAND = "generate";
  static final String PRINT_GRAPH_COMMAND = "print-graph";
  static final String INIT_COMMAND = "init";
  static final String HASH_COMMAND = "hash";
  static final String INFO_COMMAND = "info";
  private static final Map> COMMAND_MAP =
    Collections.unmodifiableMap( new HashMap>()
    {
      {
        put( GENERATE_COMMAND, GenerateCommand::new );
        put( PRINT_GRAPH_COMMAND, PrintGraphCommand::new );
        put( HASH_COMMAND, HashCommand::new );
        put( INFO_COMMAND, InfoCommand::new );
        put( INIT_COMMAND, InitCommand::new );
      }
    } );
  private static final Set VALID_COMMANDS = Collections.unmodifiableSet( COMMAND_MAP.keySet() );

  public static void main( final String[] args )
  {
    final Environment environment =
      new Environment( System.console(), Paths.get( "" ).toAbsolutePath(), Logger.getGlobal() );
    setupLogger( environment );
    System.exit( run( environment, args ) );
  }

  static int run( @Nonnull final Environment environment, @Nonnull final String... args )
  {
    if ( !processOptions( environment, args ) )
    {
      return ExitCodes.ERROR_PARSING_ARGS_EXIT_CODE;
    }

    try
    {
      return environment.getCommand().run( new CommandContextImpl( environment ) );
    }
    catch ( final InvalidModelException ime )
    {
      final Logger logger = environment.logger();
      final String message = ime.getMessage();
      if ( null != message )
      {
        logger.log( Level.WARNING, message, ime.getCause() );
      }

      logger.log( Level.WARNING,
                  "--- Invalid Config ---\n" +
                  YamlUtil.asYamlString( ime.getModel() ) +
                  "--- End Config ---" );

      return ExitCodes.ERROR_CONSTRUCTING_MODEL_CODE;
    }
    catch ( final TerminalStateException tse )
    {
      final String message = tse.getMessage();
      if ( null != message )
      {
        final Logger logger = environment.logger();
        logger.log( Level.WARNING, message );
        final Throwable cause = tse.getCause();
        if ( null != cause )
        {
          if ( logger.isLoggable( Level.INFO ) )
          {
            logger.log( Level.INFO, "Cause: " + cause.toString() );
            if ( logger.isLoggable( Level.FINE ) )
            {
              logger.log( Level.FINE, null, cause );
            }
          }
        }
      }
      return tse.getExitCode();
    }
    catch ( final Throwable t )
    {
      environment.logger().log( Level.WARNING, t.toString(), t );
      return ExitCodes.ERROR_EXIT_CODE;
    }
  }

  @Nonnull
  static ApplicationModel loadModel( @Nonnull final Environment environment )
  {
    return ApplicationModel.parse( loadConfigFile( environment ), environment.shouldResetCachedMetadata() );
  }

  @Nonnull
  static ApplicationRecord loadRecord( @Nonnull final Environment environment )
    throws DependencyResolutionException
  {
    final ApplicationModel model = loadModel( environment );
    final Resolver resolver =
      ResolverUtil.createResolver( environment,
                                   getCacheDirectory( environment, model ),
                                   model,
                                   loadSettings( environment ) );
    final ApplicationRecord record =
      ApplicationRecord.build( model,
                               resolveModel( environment, resolver, model ),
                               resolver.getAuthenticationContexts(),
                               m -> environment.logger().warning( m ) );
    cacheArtifactsInRepositoryCache( environment, record );
    return record;
  }

  static void cacheArtifactsInRepositoryCache( @Nonnull final Environment environment,
                                               @Nonnull final ApplicationRecord record )
  {
    final Path repositoryCache = BazelUtil.getRepositoryCache( environment.currentDirectory().toFile() );
    if ( null != repositoryCache )
    {
      // We only attempt to copy into repositoryCache if there is one ... which there
      // always is if there is a local WORKSPACE
      for ( final ArtifactRecord artifact : record.getArtifacts() )
      {
        if ( null == artifact.getReplacementModel() )
        {
          final Artifact a = artifact.getArtifact();
          final File file = a.getFile();
          assert null != file;
          final String sha256 = artifact.getSha256();
          assert null != sha256;
          cacheRepositoryFile( environment.logger(), repositoryCache, a.toString(), file, sha256 );
          final String sourceSha256 = artifact.getSourceSha256();
          if ( null != sourceSha256 )
          {
            final SubArtifact sourcesArtifact = new SubArtifact( a, "sources", "jar" );
            final String localFilename = ArtifactUtil.artifactToLocalFilename( sourcesArtifact );
            final File sourcesFile = file.toPath().getParent().resolve( localFilename ).toFile();

            cacheRepositoryFile( environment.logger(),
                                 repositoryCache,
                                 sourcesArtifact.toString(),
                                 sourcesFile,
                                 sourceSha256 );
          }
        }
      }
    }
  }

  static void cacheRepositoryFile( @Nonnull final Logger logger,
                                   @Nonnull final Path repositoryCache,
                                   @Nonnull final String label,
                                   @Nonnull final File file,
                                   @Nonnull final String sha256 )
  {
    final Path targetPath =
      repositoryCache.resolve( "content_addressable" ).resolve( "sha256" ).resolve( sha256 ).resolve( "file" );
    if ( !Files.exists( targetPath ) )
    {
      try
      {
        Files.createDirectories( targetPath.getParent() );
        Files.copy( file.toPath(), targetPath );
        logger.log( Level.FINE, "Installed artifact '" + label + "' into repository cache." );
      }
      catch ( final IOException ioe )
      {
        final String message = "Failed to cache artifact '" + label + "' in repository cache.";
        logger.log( Level.WARNING, message, ioe );
      }
    }
  }

  @Nonnull
  static Path getCacheDirectory( @Nonnull final Environment environment, @Nonnull final ApplicationModel model )
  {
    if ( environment.hasCacheDir() )
    {
      return environment.getCacheDir();
    }
    else
    {
      final File repositoryCache = BazelUtil.getOutputBase( model.getOptions().getWorkspaceDirectory().toFile() );
      if ( null == repositoryCache )
      {
        throw new TerminalStateException( "Error: Cache directory not specified and unable to derive default " +
                                          "directory (Is the bazel command on the path?). Explicitly pass the " +
                                          "cache directory as an option.",
                                          ExitCodes.ERROR_INVALID_DEFAULT_CACHE_DIR_CODE );
      }
      else
      {
        return repositoryCache.toPath().resolve( ".depgen-cache" );
      }
    }
  }

  @Nonnull
  private static DependencyNode resolveModel( @Nonnull final Environment environment,
                                              @Nonnull final Resolver resolver,
                                              @Nonnull final ApplicationModel model )
    throws DependencyResolutionException
  {
    final Logger logger = environment.logger();
    final DependencyResult result = resolver.resolveDependencies( model, ( artifactModel, exceptions ) -> {
      // If we get here then the listener has already emitted a warning message so just need to exit
      // We can only get here if either failOnMissingPom or failOnInvalidPom is true and an error occurred
      throw new TerminalStateException( ExitCodes.ERROR_INVALID_POM_CODE );
    } );

    final List cycles = result.getCycles();
    if ( !cycles.isEmpty() )
    {
      logger.warning( cycles.size() + " dependency cycles detected when collecting dependencies:" );
      for ( final DependencyCycle cycle : cycles )
      {
        logger.warning( cycle.toString() );
      }
      throw new TerminalStateException( ExitCodes.ERROR_CYCLES_PRESENT_CODE );
    }
    final List exceptions = result.getCollectExceptions();
    if ( !exceptions.isEmpty() )
    {
      logger.warning( exceptions.size() + " errors collecting dependencies:" );
      for ( final Exception exception : exceptions )
      {
        logger.log( Level.WARNING, null, exception );
      }
      throw new TerminalStateException( ExitCodes.ERROR_COLLECTING_DEPENDENCIES_CODE );
    }

    return result.getRoot();
  }

  @Nonnull
  private static Settings loadSettings( @Nonnull final Environment environment )
  {
    final Path settingsFile = environment.getSettingsFile();
    try
    {
      return SettingsUtil.loadSettings( settingsFile, environment.logger() );
    }
    catch ( final SettingsBuildingException e )
    {
      throw new TerminalStateException( "Error: Problem loading settings from " + settingsFile,
                                        ExitCodes.ERROR_LOADING_SETTINGS_CODE );
    }
  }

  @Nonnull
  static ApplicationConfig loadConfigFile( @Nonnull final Environment environment )
  {
    final Path dependenciesFile = environment.getConfigFile();
    try
    {
      return ApplicationConfig.parse( dependenciesFile );
    }
    catch ( final Throwable t )
    {
      throw new TerminalStateException( "Error: Failed to read dependencies file " + dependenciesFile,
                                        t,
                                        ExitCodes.ERROR_PARSING_DEPENDENCIES_CODE );
    }
  }

  static void setupLogger( @Nonnull final Environment environment )
  {
    final ConsoleHandler handler = new ConsoleHandler();
    handler.setFormatter( new RawFormatter() );
    handler.setLevel( Level.ALL );
    final Logger logger = environment.logger();
    logger.setUseParentHandlers( false );
    logger.addHandler( handler );
    logger.setLevel( Level.INFO );
  }

  static boolean processOptions( @Nonnull final Environment environment, @Nonnull final String... args )
  {
    // Parse the arguments
    final CLArgsParser parser =
      new CLArgsParser( args, OPTIONS, lastOptionCode -> CLOption.TEXT_ARGUMENT == lastOptionCode );

    //Make sure that there was no errors parsing arguments
    final Logger logger = environment.logger();
    if ( null != parser.getErrorString() )
    {
      logger.log( Level.SEVERE, "Error: " + parser.getErrorString() );
      return false;
    }
    // Retrieve run directory first as some of the other options are interpreted relative to current directory
    for ( final CLOption option : parser.getArguments() )
    {
      if ( RUN_DIR_OPT == option.getId() )
      {
        final String argument = option.getArgument();
        final Path directory = environment.currentDirectory().resolve( argument ).toAbsolutePath().normalize();
        if ( !Files.exists( directory ) )
        {
          logger.log( Level.SEVERE,
                      "Error: Specified directory does not exist. Specified value: " + argument );
          return false;
        }
        if ( !Files.isDirectory( directory ) )
        {
          logger.log( Level.SEVERE,
                      "Error: Specified directory is not a directory. Specified value: " + argument );
          return false;
        }
        environment.setCurrentDirectory( directory );
      }
    }
    // Get a list of parsed options
    for ( final CLOption option : parser.getArguments() )
    {
      switch ( option.getId() )
      {
        case CLOption.TEXT_ARGUMENT:
        {
          final String command = option.getArgument();
          if ( !VALID_COMMANDS.contains( command ) )
          {
            logger.log( Level.SEVERE, "Error: Unknown command: " + command );
            return false;
          }
          else if ( environment.hasCommand() )
          {
            logger.log( Level.SEVERE, "Error: Duplicate command specified: " + command );
            return false;
          }
          environment.setCommand( COMMAND_MAP.get( command ).get() );
          break;
        }
        case RUN_DIR_OPT:
        {
          break;
        }
        case CONFIG_FILE_OPT:
        {
          final String argument = option.getArgument();
          final Path configFile = environment.currentDirectory().resolve( argument ).toAbsolutePath().normalize();
          if ( !configFile.toFile().exists() )
          {
            logger.log( Level.SEVERE,
                        "Error: Specified config file does not exist. Specified value: " + argument );
            return false;
          }
          environment.setConfigFile( configFile );
          break;
        }
        case SETTINGS_FILE_OPT:
        {
          final String argument = option.getArgument();
          final Path settingsFile = environment.currentDirectory().resolve( argument ).toAbsolutePath().normalize();
          if ( !settingsFile.toFile().exists() )
          {
            logger.log( Level.SEVERE,
                        "Error: Specified settings file does not exist. Specified value: " + argument );
            return false;
          }
          environment.setSettingsFile( settingsFile );
          break;
        }

        case CACHE_DIR_OPT:
        {
          final String argument = option.getArgument();
          final Path cacheDir = environment.currentDirectory().resolve( argument ).toAbsolutePath().normalize();
          final File dir = cacheDir.toFile();
          if ( dir.exists() && !dir.isDirectory() )
          {
            logger.log( Level.SEVERE,
                        "Error: Specified cache directory exists but is not a directory. Specified value: " +
                        argument );
            return false;
          }
          environment.setCacheDir( cacheDir );
          break;
        }
        case RESET_CACHED_METADATA_OPT:
        {
          environment.markResetCachedMetadata();
          break;
        }

        case VERBOSE_OPT:
        {
          logger.setLevel( Level.ALL );
          break;
        }
        case QUIET_OPT:
        {
          logger.setLevel( Level.WARNING );
          break;
        }
        case VERSION_OPT:
        {
          environment.logger().log( Level.WARNING, "DepGen Version: " + DepGenConfig.getVersion() );
          return false;
        }
        case HELP_OPT:
        {
          printUsage( environment );
          return false;
        }
      }
    }

    if ( !environment.hasCommand() )
    {
      logger.log( Level.SEVERE, "Error: No command specified. Please specify a command." );
      return false;
    }
    final String[] unParsedArgs = parser.getUnParsedArgs();
    if ( unParsedArgs.length > 0 )
    {
      if ( !environment.getCommand().processOptions( environment, unParsedArgs ) )
      {
        return false;
      }
    }

    if ( !environment.hasConfigFile() )
    {
      final Path dependenciesFile =
        environment.currentDirectory()
          .resolve( ApplicationConfig.DEFAULT_MODULE )
          .resolve( ApplicationConfig.FILENAME )
          .toAbsolutePath()
          .normalize();
      if ( !dependenciesFile.toFile().exists() )
      {
        logger.log( Level.SEVERE,
                    "Error: Default config file does not exist: " +
                    ApplicationConfig.DEFAULT_MODULE + "/" + ApplicationConfig.FILENAME );
        return false;
      }
      environment.setConfigFile( dependenciesFile );
    }

    if ( !environment.hasSettingsFile() )
    {
      final Path settingsFile =
        Paths.get( System.getProperty( "user.home" ), ".m2", "settings.xml" ).toAbsolutePath().normalize();
      environment.setSettingsFile( settingsFile );
    }

    return true;
  }

  /**
   * Print out a usage statement
   */
  static void printUsage( @Nonnull final Environment environment )
  {
    final Logger logger = environment.logger();
    logger.severe( "java " + Main.class.getName() + " [options] [command]" );
    logger.severe( "\tPossible Commands:" );
    logger.severe( "\t\t" + GENERATE_COMMAND + ": Generate the bazel extension from the dependency configuration." );
    logger.severe( "\t\t" + PRINT_GRAPH_COMMAND + ": Compute and print the dependency graph " +
                   "for the dependency configuration." );
    logger.severe( "\t\t" + HASH_COMMAND + ": Generate a hash of the content of the dependency configuration." );
    logger.severe( "\t\t" +
                   INIT_COMMAND +
                   ": Initialize an empty dependency configuration and workspace infrastructure." );
    logger.severe( "\t\t" + INFO_COMMAND + ": Print runtime info about the tool." );
    logger.severe( "\tOptions:" );
    final String[] options =
      CLUtil.describeOptions( OPTIONS ).toString().split( System.getProperty( "line.separator" ) );
    for ( final String line : options )
    {
      logger.severe( line );
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy