001/**
002 * Copyright (C) 2009-2011 FuseSource Corp.
003 * http://fusesource.com
004 * 
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 * 
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.fusesource.hawtjni.maven;
018
019import java.io.*;
020import java.net.URL;
021import java.util.List;
022
023import org.apache.maven.artifact.Artifact;
024import org.apache.maven.artifact.factory.ArtifactFactory;
025import org.apache.maven.artifact.repository.ArtifactRepository;
026import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
027import org.apache.maven.artifact.resolver.ArtifactResolutionException;
028import org.apache.maven.artifact.resolver.ArtifactResolver;
029import org.apache.maven.model.Dependency;
030import org.apache.maven.model.Resource;
031import org.apache.maven.plugin.AbstractMojo;
032import org.apache.maven.plugin.MojoExecutionException;
033import org.apache.maven.plugins.annotations.Component;
034import org.apache.maven.plugins.annotations.LifecyclePhase;
035import org.apache.maven.plugins.annotations.Mojo;
036import org.apache.maven.plugins.annotations.Parameter;
037import org.apache.maven.project.MavenProject;
038import org.codehaus.plexus.archiver.UnArchiver;
039import org.codehaus.plexus.archiver.manager.ArchiverManager;
040import org.codehaus.plexus.util.FileUtils;
041import org.codehaus.plexus.util.IOUtil;
042import org.codehaus.plexus.util.cli.CommandLineException;
043import org.fusesource.hawtjni.runtime.Library;
044
045/**
046 * This goal builds the JNI module which was previously
047 * generated with the generate goal.  It adds the JNI module
048 * to the test resource path so that unit tests can load 
049 * the freshly built JNI library.
050 * 
051 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
052 */
053@Mojo(name = "build", defaultPhase = LifecyclePhase.GENERATE_TEST_RESOURCES)
054public class BuildMojo extends AbstractMojo {
055
056    /**
057     * The maven project.
058     */
059    @Parameter(defaultValue = "${project}", readonly = true)
060    protected MavenProject project;
061    
062    /**
063     * Remote repositories
064     */
065    @Parameter(defaultValue = "${project.remoteArtifactRepositories}", readonly = true)
066    protected List remoteArtifactRepositories;
067
068    /**
069     * Local maven repository.
070     */
071    @Parameter(defaultValue = "${localRepository}", readonly = true)
072    protected ArtifactRepository localRepository;
073
074    /**
075     * Artifact factory, needed to download the package source file
076     */
077    @Component
078    protected ArtifactFactory artifactFactory;
079
080    /**
081     * Artifact resolver, needed to download the package source file
082     */
083    @Component
084    protected ArtifactResolver artifactResolver;
085    
086    /**
087     */
088    @Component
089    private ArchiverManager archiverManager;    
090
091    /**
092     * The base name of the library, used to determine generated file names.
093     */
094    @Parameter(defaultValue = "${project.artifactId}")
095    private String name;
096    
097    /**
098     * Where the unpacked build package is located.
099     */
100    @Parameter(defaultValue = "${project.build.directory}/generated-sources/hawtjni/native-package")
101    private File packageDirectory;
102
103    /**
104     * The output directory where the built JNI library will placed.  This directory will be added
105     * to as a test resource path so that unit tests can verify the built JNI library.
106     * 
107     * The library will placed under the META-INF/native/${platform} directory that the HawtJNI
108     * Library uses to find JNI libraries as classpath resources.
109     */
110    @Parameter(defaultValue = "${project.build.directory}/generated-sources/hawtjni/lib")
111    private File libDirectory;
112
113    /**
114     * The directory where the build will be produced.  It creates a native-build and native-dist directory
115     * under the specified directory.
116     */
117    @Parameter(defaultValue = "${project.build.directory}")
118    private File buildDirectory;
119
120    /**
121     * Should we skip executing the autogen.sh file.
122     */
123    @Parameter(defaultValue = "${skip-autogen}")
124    private boolean skipAutogen;
125    
126    /**
127     * Should we force executing the autogen.sh file.
128     */
129    @Parameter(defaultValue = "${force-autogen}")
130    private boolean forceAutogen;
131    
132    /**
133     * Extra arguments you want to pass to the autogen.sh command.
134     */
135    @Parameter
136    private List<String> autogenArgs;
137
138    /**
139     * Should we skip executing the configure command.
140     */
141    @Parameter(defaultValue = "${skip-configure}")
142    private boolean skipConfigure;
143
144    /**
145     * Should we force executing the configure command.
146     */
147    @Parameter(defaultValue = "${force-configure}")
148    private boolean forceConfigure;
149    
150    /**
151     * Should we display all the native build output?
152     */
153    @Parameter(defaultValue = "${hawtjni-verbose}")
154    private boolean verbose;
155
156    /**
157     * Extra arguments you want to pass to the configure command.
158     */
159    @Parameter
160    private List<String> configureArgs;
161    
162    /**
163     * The platform identifier of this build.  If not specified,
164     * it will be automatically detected.
165     */
166    @Parameter
167    private String platform;    
168    
169    /**
170     * The classifier of the package archive that will be created.
171     */
172    @Parameter(defaultValue = "native-src")
173    private String sourceClassifier;  
174    
175    /**
176     * If the source build could not be fully generated, perhaps the autotools
177     * were not available on this platform, should we attempt to download
178     * a previously deployed source package and build that?
179     */
180    @Parameter(defaultValue = "true")
181    private boolean downloadSourcePackage = true;  
182
183    /**
184     * The dependency to download to get the native sources.
185     */
186    @Parameter
187    private Dependency nativeSrcDependency;
188
189    /**
190     * URL to where we can down the source package
191     */
192    @Parameter(defaultValue = "${native-src-url}")
193    private String nativeSrcUrl;
194
195    /**
196     * The build tool to use on Windows systems.  Set
197     * to 'msbuild', 'vcbuild', or 'detect'
198     */
199    @Parameter(defaultValue = "detect")
200    private String windowsBuildTool;
201
202    /**
203     * The name of the msbuild/vcbuild project to use.
204     * Defaults to 'vs2010' for 'msbuild'
205     * and 'vs2008' for 'vcbuild'.
206     */
207    @Parameter
208    private String windowsProjectName;
209
210    private final CLI cli = new CLI();
211
212    public void execute() throws MojoExecutionException {
213        cli.verbose = verbose;
214        cli.log = getLog();
215        try {
216            File buildDir = new File(buildDirectory, "native-build");
217            buildDir.mkdirs();
218            if ( CLI.IS_WINDOWS ) {
219                vsBasedBuild(buildDir);
220            } else {
221                configureBasedBuild(buildDir);
222            }
223            
224            getLog().info("Adding test resource root: "+libDirectory.getAbsolutePath());
225            Resource testResource = new Resource();
226            testResource.setDirectory(libDirectory.getAbsolutePath());
227            this.project.addTestResource(testResource); //();
228            
229        } catch (Exception e) {
230            throw new MojoExecutionException("build failed: "+e, e);
231        } 
232    }
233
234    private void vsBasedBuild(File buildDir) throws CommandLineException, MojoExecutionException, IOException {
235        
236        FileUtils.copyDirectoryStructureIfModified(packageDirectory, buildDir);
237
238        Library library = new Library(name);
239        String libPlatform = this.platform != null ? this.platform : Library.getPlatform();
240        String platform;
241        String configuration="release";
242        if( "windows32".equals(libPlatform) ) {
243                platform = "Win32";
244        } else if( "windows64".equals(libPlatform) ) {
245                platform = "x64";
246        } else {
247                throw new MojoExecutionException("Unsupported platform: "+libPlatform);
248        }
249
250        boolean useMSBuild = false;
251        String tool = windowsBuildTool.toLowerCase().trim();
252        if( "detect".equals(tool) ) {
253            String toolset = System.getenv("PlatformToolset");
254            if( "Windows7.1SDK".equals(toolset) ) {
255                useMSBuild = true;
256            } else {
257                String vcinstalldir = System.getenv("VCINSTALLDIR");
258                if( vcinstalldir!=null ) {
259                    if( vcinstalldir.contains("Microsoft Visual Studio 10") ||
260                        vcinstalldir.contains("Microsoft Visual Studio 11") ||
261                        vcinstalldir.contains("Microsoft Visual Studio 12")
262                      ) {
263                        useMSBuild = true;
264                    }
265                }
266            }
267        } else if( "msbuild".equals(tool) ) {
268            useMSBuild = true;
269        } else if( "vcbuild".equals(tool) ) {
270            useMSBuild = false;
271        } else {
272            throw new MojoExecutionException("Invalid setting for windowsBuildTool: "+windowsBuildTool);
273        }
274
275        if( useMSBuild ) {
276            // vcbuild was removed.. use the msbuild tool instead.
277            int rc = cli.system(buildDir, new String[]{"msbuild", (windowsProjectName != null ? windowsProjectName : "vs2010") + ".vcxproj", "/property:Platform="+platform, "/property:Configuration="+configuration});
278            if( rc != 0 ) {
279                throw new MojoExecutionException("vcbuild failed with exit code: "+rc);
280            }
281        } else {
282            // try to use a vcbuild..
283            int rc = cli.system(buildDir, new String[]{"vcbuild", "/platform:"+platform, (windowsProjectName != null ? windowsProjectName : "vs2008") + ".vcproj", configuration});
284            if( rc != 0 ) {
285                throw new MojoExecutionException("vcbuild failed with exit code: "+rc);
286            }
287        }
288
289
290
291        File libFile=FileUtils.resolveFile(buildDir, "target/"+platform+"-"+configuration+"/lib/"+library.getLibraryFileName());
292        if( !libFile.exists() ) {
293            throw new MojoExecutionException("vcbuild did not generate: "+libFile);
294        }        
295
296        File target=FileUtils.resolveFile(libDirectory, library.getPlatformSpecificResourcePath(libPlatform));
297        FileUtils.copyFile(libFile, target);
298
299        }
300
301    
302        private void configureBasedBuild(File buildDir) throws IOException, MojoExecutionException, CommandLineException {
303        
304        File configure = new File(packageDirectory, "configure");
305        if( configure.exists() ) {
306            FileUtils.copyDirectoryStructureIfModified(packageDirectory, buildDir);            
307        } else if (downloadSourcePackage) {
308            downloadNativeSourcePackage(buildDir);
309        } else {
310            if( !buildDir.exists() ) {
311                throw new MojoExecutionException("The configure script is missing from the generated native source package and downloadSourcePackage is disabled: "+configure);
312            }
313        }
314
315        configure = new File(buildDir, "configure");
316        File autogen = new File(buildDir, "autogen.sh");
317        File makefile = new File(buildDir, "Makefile");
318        
319        File distDirectory = new File(buildDir, "target");
320        File distLibDirectory = new File(distDirectory, "lib");
321                distLibDirectory.mkdirs();
322        
323        if( autogen.exists() && !skipAutogen ) {
324            if( (!configure.exists() && !CLI.IS_WINDOWS) || forceAutogen ) {
325                cli.setExecutable(autogen);
326                int rc = cli.system(buildDir, new String[] {"./autogen.sh"}, autogenArgs);
327                if( rc != 0 ) {
328                    throw new MojoExecutionException("./autogen.sh failed with exit code: "+rc);
329                }
330            }
331        }
332        
333        if( configure.exists() && !skipConfigure ) {
334            if( !makefile.exists() || forceConfigure ) {
335                
336                File autotools = new File(buildDir, "autotools");
337                File[] listFiles = autotools.listFiles();
338                if( listFiles!=null ) {
339                    for (File file : listFiles) {
340                        cli.setExecutable(file);
341                    }
342                }
343                
344                cli.setExecutable(configure);
345                int rc = cli.system(buildDir, new String[]{"./configure", "--disable-ccache", "--prefix="+distDirectory.getCanonicalPath(), "--libdir="+distDirectory.getCanonicalPath()+"/lib"}, configureArgs);
346                if( rc != 0 ) {
347                    throw new MojoExecutionException("./configure failed with exit code: "+rc);
348                }
349            }
350        }
351        
352        int rc = cli.system(buildDir, new String[]{"make", "install"});
353        if( rc != 0 ) {
354            throw new MojoExecutionException("make based build failed with exit code: "+rc);
355        }
356        
357        Library library = new Library(name);
358        
359        File libFile = new File(distLibDirectory, library.getLibraryFileName());
360        if( !libFile.exists() ) {
361            throw new MojoExecutionException("Make based build did not generate: "+libFile);
362        }
363        
364        if( platform == null ) {
365            platform = library.getPlatform();
366        }
367        
368        File target=FileUtils.resolveFile(libDirectory, library.getPlatformSpecificResourcePath(platform));
369        FileUtils.copyFile(libFile, target);
370    }
371    
372    public void downloadNativeSourcePackage(File buildDir) throws MojoExecutionException  {
373        File packageZipFile;
374        if( nativeSrcUrl ==null || nativeSrcUrl.trim().length()==0 ) {
375            Artifact artifact=null;
376            if( nativeSrcDependency==null ) {
377                artifact = artifactFactory.createArtifactWithClassifier(project.getGroupId(), project.getArtifactId(), project.getVersion(), "zip", sourceClassifier);
378            } else {
379                artifact = artifactFactory.createArtifactWithClassifier(nativeSrcDependency.getGroupId(), nativeSrcDependency.getArtifactId(), nativeSrcDependency.getVersion(), nativeSrcDependency.getType(), nativeSrcDependency.getClassifier());
380            }
381            try {
382                artifactResolver.resolveAlways(artifact, remoteArtifactRepositories, localRepository);
383            } catch (ArtifactResolutionException e) {
384                throw new MojoExecutionException("Error downloading.", e);
385            } catch (ArtifactNotFoundException e) {
386                throw new MojoExecutionException("Requested download does not exist.", e);
387            }
388
389            packageZipFile = artifact.getFile();
390            if( packageZipFile.isDirectory() ) {
391                // Yep. looks like we are running on mvn 3, seem like
392                // mvn 3 does not actually download the artifact. it just points us
393                // to our own build.
394                throw new MojoExecutionException("Add a '-Dnative-src-url=file:...' to have maven download the native package");
395            }
396        } else {
397            try {
398                packageZipFile = new File(buildDirectory, "native-build.zip");
399                URL url = new URL(nativeSrcUrl.trim());
400                InputStream is = url.openStream();
401                try {
402                    FileOutputStream os = new FileOutputStream(packageZipFile);
403                    try {
404                        IOUtil.copy(is, os);
405                    } finally {
406                        IOUtil.close(is);
407                    }
408
409                } finally {
410                    IOUtil.close(is);
411                }
412            } catch (Exception e) {
413                throw new MojoExecutionException("Error downloading: "+ nativeSrcUrl, e);
414            }
415        }
416
417        try {
418            File dest = new File(buildDirectory, "native-build-extracted");
419            getLog().info("Extracting "+packageZipFile+" to "+dest);
420            
421            UnArchiver unArchiver = archiverManager.getUnArchiver("zip");
422            unArchiver.setSourceFile(packageZipFile);
423            unArchiver.extract("", dest);
424
425
426            File source = findSourceRoot(dest);
427            if( source==null ) {
428                throw new MojoExecutionException("Extracted package did not look like it contained a native source build.");
429            }
430            FileUtils.copyDirectoryStructureIfModified(source, buildDir);            
431            
432        } catch (MojoExecutionException e) {
433            throw e;
434        } catch (Throwable e) {
435            throw new MojoExecutionException("Could not extract the native source package.", e);
436        }            
437    }
438
439    private File findSourceRoot(File dest) {
440        if(dest.isDirectory()) {
441            if( new File(dest, "configure").exists() ) {
442                return dest;
443            } else {
444                for (File file : dest.listFiles()) {
445                    File root = findSourceRoot(file);
446                    if( root!=null ) {
447                        return root;
448                    }
449                }
450                return null;
451            }
452        } else {
453            return null;
454        }
455    }
456
457}