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