package de.saumya.mojo.gem; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.net.URL; import java.sql.Date; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; import org.apache.maven.artifact.resolver.ArtifactResolutionResult; import org.apache.maven.artifact.resolver.filter.ArtifactFilter; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.StringUtils; import de.saumya.mojo.ruby.script.ScriptException; /** * goal to convert that artifact into a gem or uses a given gemspec to build a gem. */ @Mojo( name = "package", requiresDependencyResolution = ResolutionScope.TEST, requiresProject = false ) public class PackageMojo extends AbstractGemMojo { @Parameter( defaultValue = "${project.build.directory}" ) File buildDirectory; /** * the gemspec to use for building the gem */ @Parameter( property = "gemspec" ) File gemspec; /** * use the pom to generate a gemspec and overwrite the one in lauchDirectory. */ @Parameter( property = "gemspec.overwrite" ) boolean gemspecOverwrite = false; @Parameter private String date; @Parameter private String extraRdocFiles; @Parameter private String extraFiles; @Parameter private String rdocOptions; @Parameter private String requirePaths; @Parameter private String rubyforgeProject; @Parameter private String rubygemsVersion; @Parameter private String requiredRubygemsVersion; @Parameter private String bindir; @Parameter private String requiredRubyVersion; @Parameter private String postInstallMessage; @Parameter private String executables; @Parameter private String extensions; @Parameter private String platform; @Parameter( defaultValue = "gem_hook.rb" ) private String gemHook; @Parameter( defaultValue = "false" ) boolean includeDependencies; /** * use repository layout for included dependencies */ @Parameter( defaultValue = "false" ) boolean useRepositoryLayout; private void generatePom(File source, File target) throws ScriptException, IOException { URL url = Thread.currentThread().getContextClassLoader().getResource("maven/tools/pom.rb"); String baseUrl = url.toExternalForm().replace("/maven/tools/pom.rb", ""); // this assumes to get an url JRuby understands !! this.factory.newScript("def warn(*args);end;$LOAD_PATH << '" + baseUrl + "';" + "require 'maven/tools/pom';" + "puts Maven::Tools::POM.new('" + source.getAbsolutePath() + "').to_s") .executeIn(launchDirectory(), target); } @Override public void executeJRuby() throws MojoExecutionException, MojoFailureException, ScriptException { final MavenProject project = this.project; final GemArtifact artifact = new GemArtifact(project); try { // first case no gemspec file given but there is pom => use pom to generate gemspec if (this.gemspec == null && project.getBasedir() != null && project.getBasedir().exists()) { // TODO generate the gemspec in the prepare-package phase so we // can use it separately buildFromPom(project, artifact); // use POM generated by polyglot maven instead of project // file (which can be 'Gemfile' or a gemspec file) to allow // to install it into the local repository and be // usable for proper maven // maybe that is a bug in polyglot maven ? final File pom = new File(this.project.getFile() .getAbsolutePath() + ".pom"); if (pom.exists()) { this.project.setFile(pom); } } else { // no gemspec file given => find a gemspec file in launchDirectory if (this.gemspec == null) { for (final File f : launchDirectory().listFiles()) { if (f.getName().endsWith(".gemspec")) { if (this.gemspec == null) { this.gemspec = f; } else { throw new MojoFailureException("more than one gemspec file found, use -Dgemspec=... to specifiy one"); } } } if (this.gemspec == null) { throw new MojoFailureException("no gemspec file or pom found, use -Dgemspec=... to specifiy a gemspec file or '-f ...' to use a pom file"); } else { getLog().info("use gemspec: " + this.gemspec); } } ArtifactResolutionResult jarDependencyArtifacts = includeDependencies( project, artifact ); if (jarDependencyArtifacts != null ) { getLog().info("use repository layout? " + this.useRepositoryLayout); for (final Object element : jarDependencyArtifacts.getArtifacts()) { final Artifact dependency = (Artifact) element; getLog().info(" -- include -- " + dependency); File target; if ( useRepositoryLayout ) { StringBuilder path = new StringBuilder( libDirectory.getAbsolutePath() ); path.append( File.separatorChar ).append( dependency.getGroupId().replace( ".", File.separator ) ); path.append( File.separatorChar ).append( dependency.getArtifactId() ); path.append( File.separatorChar ).append( dependency.getVersion() ); target = new File( path.toString(), dependency.getFile().getName() ); target.getParentFile().mkdirs(); } else { target = new File( libDirectory, dependency.getFile().getName() ); } FileUtils.copyFile( dependency.getFile(), target ); } } // now we have a gemspec file - either found or given this.factory.newScriptFromJRubyJar("gem") .addArg("build", this.gemspec) .executeIn(launchDirectory()); File newPom = new File( this.gemspec.getParentFile(), "pom." + this.gemspec.getName() + ".xml"); generatePom(this.gemspec, newPom); project.setFile(newPom); // find file with biggest lastModified File gemFile = null; for (final File f : launchDirectory().listFiles()) { if (f.getName().endsWith(".gem")) { if (gemFile == null || gemFile.lastModified() < f.lastModified() ) { gemFile = f; } } } if (project.getFile() != null && artifact.isGem()) { // only when the pom exist there will be an artifact FileUtils.copyFileIfModified(gemFile, artifact.getFile()); gemFile.delete(); } else { // keep the gem where it is when there is no buildDirectory if (this.buildDirectory.exists()) { FileUtils.copyFileIfModified(gemFile, new File(this.buildDirectory, gemFile.getName())); gemFile.deleteOnExit(); } } } } catch (final IOException e) { throw new MojoExecutionException("error gemifing pom", e); } } private void buildFromPom(final MavenProject project, final GemArtifact artifact) throws MojoExecutionException, IOException, ScriptException { getLog().info("building gem for " + artifact + " . . ."); final File gemDir = new File(this.buildDirectory, artifact.getGemName()); final File gemSpec = new File(gemDir, artifact.getGemName() + ".gemspec"); final GemspecWriter gemSpecWriter = new GemspecWriter(gemSpec, project, artifact); if (this.date != null) { gemSpecWriter.append("date", Date.valueOf(this.date).toString()); } gemSpecWriter.append("rubygems_version", this.rubygemsVersion); gemSpecWriter.append("required_rubygems_version", this.requiredRubygemsVersion); gemSpecWriter.append("required_ruby_version", this.requiredRubyVersion); gemSpecWriter.append("bindir", this.bindir); gemSpecWriter.append("post_install_message", this.postInstallMessage); gemSpecWriter.append("rubyforge_project", this.rubyforgeProject); gemSpecWriter.appendRdocFiles(this.extraRdocFiles); gemSpecWriter.appendFiles(this.extraFiles); gemSpecWriter.appendList("executables", this.executables); gemSpecWriter.appendList("extensions", this.extensions); gemSpecWriter.appendList("rdoc_options", this.rdocOptions); gemSpecWriter.appendList("require_paths", this.requirePaths); final File rubyFile; if (artifact.hasJarFile()) { gemSpecWriter.appendPlatform(this.platform == null ? "java" : this.platform); gemSpecWriter.appendJarfile(artifact.getJarFile(), artifact.getJarFile().getName()); final File lib = new File(gemDir, "lib"); lib.mkdirs(); // need relative filename here rubyFile = new File(lib.getName(), artifact.getGemName() + ".rb"); gemSpecWriter.appendFile(rubyFile); } else { rubyFile = null; gemSpecWriter.appendPlatform(this.platform); } ArtifactResolutionResult jarDependencyArtifacts = includeDependencies( project, artifact ); if (jarDependencyArtifacts != null ) { for (final Object element : jarDependencyArtifacts.getArtifacts()) { final Artifact dependency = (Artifact) element; getLog().info(" -- include -- " + dependency); gemSpecWriter.appendJarfile(dependency.getFile(), dependency.getFile().getName()); } } // TODO make it the maven way (src/main/ruby + src/test/ruby) or the // ruby way (lib + spec + test) // TODO make a loop or so ;-) final File binDir = new File(project.getBasedir(), "bin"); final File libDir = new File(project.getBasedir(), "lib"); final File generatorsDir = new File(project.getBasedir(), "generators"); final File specDir = new File(project.getBasedir(), "spec"); final File testDir = new File(project.getBasedir(), "test"); if (binDir.exists()) { gemSpecWriter.appendPath("bin"); for (final File file : binDir.listFiles()) { // if ( file.canExecute() ) { // java1.6 feature which will fail // on jre1.5 runtimes gemSpecWriter.appendExecutable(file.getName()); // } } } if (libDir.exists()) { gemSpecWriter.appendPath("lib"); } if (generatorsDir.exists()) { gemSpecWriter.appendPath("generators"); } if (specDir.exists()) { gemSpecWriter.appendPath("spec"); gemSpecWriter.appendTestPath("spec"); } if (testDir.exists()) { gemSpecWriter.appendPath("test"); gemSpecWriter.appendTestPath("test"); } for (final Dependency dependency : project.getDependencies()) { if (!dependency.isOptional() && dependency.getType().contains("gem")) { final String prefix = dependency.getGroupId() .equals("rubygems") ? "" : dependency.getGroupId() + "."; if ((Artifact.SCOPE_COMPILE + Artifact.SCOPE_RUNTIME).contains(dependency.getScope())) { gemSpecWriter.appendDependency(prefix + dependency.getArtifactId(), dependency.getVersion()); } else if ((Artifact.SCOPE_PROVIDED + Artifact.SCOPE_TEST).contains(dependency.getScope())) { gemSpecWriter.appendDevelopmentDependency(prefix + dependency.getArtifactId(), dependency.getVersion()); } else { // TODO put things into "requirements" } } } gemSpecWriter.close(); gemSpecWriter.copy(gemDir); if (artifact.hasJarFile() && !rubyFile.exists()) { FileWriter writer = null; try { // need absolute filename here writer = new FileWriter(new File(gemDir, rubyFile.getPath())); writer.append("module ") .append(titleizedClassname(project.getArtifactId())) .append("\n"); writer.append(" VERSION = '") .append(artifact.getGemVersion()) .append("'\n"); writer.append(" MAVEN_VERSION = '") .append(project.getVersion()) .append("'\n"); writer.append("end\n"); writer.append("begin\n"); writer.append(" require 'java'\n"); writer.append(" require File.dirname(__FILE__) + '/") .append(artifact.getJarFile().getName()) .append("'\n"); if (jarDependencyArtifacts != null) { for (final Object element : jarDependencyArtifacts.getArtifacts()) { final Artifact dependency = (Artifact) element; writer.append(" require File.dirname(__FILE__) + '/") .append(dependency.getFile().getName()) .append("'\n"); } } writer.append("rescue LoadError\n"); writer.append(" puts 'JAR-based gems require JRuby to load. Please visit www.jruby.org.'\n"); writer.append(" raise\n"); writer.append("end\n"); writer.append("\n"); writer.append("load File.dirname(__FILE__) + '/" + this.gemHook + "' if File.exists?( File.dirname(__FILE__) + '/" + this.gemHook + "')\n"); } catch (final IOException e) { throw new MojoExecutionException("error writing ruby file", e); } finally { if (writer != null) { try { writer.close(); } catch (final IOException ignore) { } } } } final File localGemspec = new File(launchDirectory(), gemSpec.getName()); this.factory.newScriptFromJRubyJar("gem") .addArg("build", gemSpec) .executeIn(gemDir); if ((!localGemspec.exists() || !FileUtils.contentEquals(localGemspec, gemSpec)) && this.gemspecOverwrite) { getLog().info("overwrite gemspec '" + localGemspec.getName() + "'"); FileUtils.copyFile(gemSpec, localGemspec); } final StringBuilder gemFilename = new StringBuilder("rubygems".equals(artifact.getGroupId()) ? "" : artifact.getGroupId() + ".").append(artifact.getArtifactId()) .append("-") .append(artifact.getGemVersion()) .append("java-gem".equals(artifact.getType()) || "java".equals(this.platform) ? "-java" : "") .append(".gem"); FileUtils.copyFile(new File(gemDir, gemFilename.toString()), artifact.getFile()); } private ArtifactResolutionResult includeDependencies( MavenProject project, final GemArtifact artifact) throws IOException { ArtifactResolutionResult jarDependencyArtifacts = null; getLog().info("include dependencies? " + this.includeDependencies); if (this.includeDependencies) { final ArtifactFilter filter = new ArtifactFilter() { public boolean include(final Artifact candidate) { if (candidate == artifact) { return true; } final boolean result = (candidate.getType().equals("jar") && ("compile".equals(candidate.getScope()) || "runtime".equals(candidate.getScope()))); return result; } }; // remember file location since resolve will set it to // local-repository location final File artifactFile = artifact.getFile(); final ArtifactResolutionRequest request = new ArtifactResolutionRequest().setArtifact(project.getArtifact()) .setResolveRoot(false) .setLocalRepository(this.localRepository) .setRemoteRepositories(project.getRemoteArtifactRepositories()) .setCollectionFilter(filter) .setManagedVersionMap(project.getManagedVersionMap()) .setArtifactDependencies(project.getDependencyArtifacts()); jarDependencyArtifacts = this.repositorySystem.resolve(request); // keep the artifactFile on build directory artifact.setFile(artifactFile); } return jarDependencyArtifacts; } private String titleizedClassname(final String artifactId) { final StringBuilder name = new StringBuilder(); for (final String part : artifactId.split("-")) { name.append(StringUtils.capitalise(part)); } return name.toString(); } @Override protected void executeWithGems() throws MojoExecutionException, ScriptException, IOException { // nothing to do here since we override executeJRuby } }