001/*
002 * Copyright 2007-2018 The jdeb developers.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.vafer.jdeb.maven;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileNotFoundException;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030
031import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
032import org.apache.commons.compress.archivers.tar.TarConstants;
033import org.apache.maven.artifact.Artifact;
034import org.apache.maven.execution.MavenSession;
035import org.apache.maven.plugin.AbstractMojo;
036import org.apache.maven.plugin.MojoExecutionException;
037import org.apache.maven.plugins.annotations.Component;
038import org.apache.maven.plugins.annotations.LifecyclePhase;
039import org.apache.maven.plugins.annotations.Mojo;
040import org.apache.maven.plugins.annotations.Parameter;
041import org.apache.maven.project.MavenProject;
042import org.apache.maven.project.MavenProjectHelper;
043import org.apache.maven.settings.Profile;
044import org.apache.maven.settings.Settings;
045import org.apache.tools.tar.TarEntry;
046import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher;
047import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException;
048import org.vafer.jdeb.Console;
049import org.vafer.jdeb.DataConsumer;
050import org.vafer.jdeb.DataProducer;
051import org.vafer.jdeb.DebMaker;
052import org.vafer.jdeb.PackagingException;
053import org.vafer.jdeb.utils.MapVariableResolver;
054import org.vafer.jdeb.utils.SymlinkUtils;
055import org.vafer.jdeb.utils.Utils;
056import org.vafer.jdeb.utils.VariableResolver;
057
058import static org.vafer.jdeb.utils.Utils.isBlank;
059import static org.vafer.jdeb.utils.Utils.lookupIfEmpty;
060
061/**
062 * Creates Debian package
063 */
064@Mojo(name = "jdeb", defaultPhase = LifecyclePhase.PACKAGE, threadSafe = true)
065public class DebMojo extends AbstractMojo {
066
067    @Component
068    private MavenProjectHelper projectHelper;
069
070    @Component(hint = "jdeb-sec")
071    private SecDispatcher secDispatcher;
072
073    /**
074     * Defines the name of deb package.
075     */
076    @Parameter
077    private String name;
078
079    /**
080     * Defines the pattern of the name of final artifacts. Possible
081     * substitutions are [[baseDir]] [[buildDir]] [[artifactId]] [[version]]
082     * [[extension]] and [[groupId]].
083     */
084    @Parameter(defaultValue = "[[buildDir]]/[[artifactId]]_[[version]]_all.[[extension]]")
085    private String deb;
086
087    /**
088     * Explicitly defines the path to the control directory. At least the
089     * control file is mandatory.
090     */
091    @Parameter(defaultValue = "[[baseDir]]/src/deb/control")
092    private String controlDir;
093
094    /**
095     * Explicitly define the file to read the changes from.
096     */
097    @Parameter(defaultValue = "[[baseDir]]/CHANGES.txt")
098    private String changesIn;
099
100    /**
101     * Explicitly define the file where to write the changes to.
102     */
103    @Parameter(defaultValue = "[[buildDir]]/[[artifactId]]_[[version]]_all.changes")
104    private String changesOut;
105
106    /**
107     * Explicitly define the file where to write the changes of the changes input to.
108     */
109    @Parameter(defaultValue = "[[baseDir]]/CHANGES.txt")
110    private String changesSave;
111
112    /**
113     * The compression method used for the data file (none, gzip, bzip2 or xz)
114     */
115    @Parameter(defaultValue = "gzip")
116    private String compression;
117
118    /**
119     * Boolean option whether to attach the artifact to the project
120     */
121    @Parameter(defaultValue = "true")
122    private String attach;
123
124    /**
125     * The location where all package files will be installed. By default, all
126     * packages are installed in /opt (see the FHS here:
127     * http://www.pathname.com/
128     * fhs/pub/fhs-2.3.html#OPTADDONAPPLICATIONSOFTWAREPACKAGES)
129     */
130    @Parameter(defaultValue = "/opt/[[artifactId]]")
131    private String installDir;
132
133    /**
134     * The type of attached artifact
135     */
136    @Parameter(defaultValue = "deb")
137    private String type;
138
139    /**
140     * The project base directory
141     */
142    @Parameter(defaultValue = "${basedir}", required = true, readonly = true)
143    private File baseDir;
144
145    /**
146     * The Maven Session Object
147     */
148    @Parameter( defaultValue = "${session}", readonly = true )
149    private MavenSession session;
150
151    /**
152     * The Maven Project Object
153     */
154    @Parameter( defaultValue = "${project}", readonly = true )
155    private MavenProject project;
156
157    /**
158     * The build directory
159     */
160    @Parameter(property = "project.build.directory", required = true, readonly = true)
161    private File buildDirectory;
162
163    /**
164     * The classifier of attached artifact
165     */
166    @Parameter
167    private String classifier;
168
169    /**
170     * The digest algorithm to use.
171     *
172     * @see org.bouncycastle.bcpg.HashAlgorithmTags
173     */
174    @Parameter(defaultValue = "SHA1")
175    private String digest;
176
177    /**
178     * "data" entries used to determine which files should be added to this deb.
179     * The "data" entries may specify a tarball (tar.gz, tar.bz2, tgz), a
180     * directory, or a normal file. An entry would look something like this in
181     * your pom.xml:
182     *
183     *
184     * <pre>
185     *   <build>
186     *     <plugins>
187     *       <plugin>
188     *       <artifactId>jdeb</artifactId>
189     *       <groupId>org.vafer</groupId>
190     *       ...
191     *       <configuration>
192     *         ...
193     *         <dataSet>
194     *           <data>
195     *             <src>${project.basedir}/target/my_archive.tar.gz</src>
196     *             <include>...</include>
197     *             <exclude>...</exclude>
198     *             <mapper>
199     *               <type>perm</type>
200     *               <strip>1</strip>
201     *               <prefix>/somewhere/else</prefix>
202     *               <user>santbj</user>
203     *               <group>santbj</group>
204     *               <mode>600</mode>
205     *             </mapper>
206     *           </data>
207     *           <data>
208     *             <src>${project.build.directory}/data</src>
209     *             <include></include>
210     *             <exclude>**&#47;.svn</exclude>
211     *             <mapper>
212     *               <type>ls</type>
213     *               <src>mapping.txt</src>
214     *             </mapper>
215     *           </data>
216     *           <data>
217     *             <type>link</type>
218     *             <linkName>/a/path/on/the/target/fs</linkName>
219     *             <linkTarget>/a/sym/link/to/the/scr/file</linkTarget>
220     *             <symlink>true</symlink>
221     *           </data>
222     *           <data>
223     *             <src>${project.basedir}/README.txt</src>
224     *           </data>
225     *         </dataSet>
226     *       </configuration>
227     *     </plugins>
228     *   </build>
229     * </pre>
230     *
231     */
232    @Parameter
233    private Data[] dataSet;
234
235    /**
236     * @deprecated
237    @Parameter(defaultValue = "false")
238    private boolean timestamped;
239     */
240
241    /**
242     * When enabled SNAPSHOT inside the version gets replaced with current timestamp or
243     * if set a value of a environment variable.
244     */
245    @Parameter(defaultValue = "false")
246    private boolean snapshotExpand;
247
248    /**
249     * Which environment variable to check for the SNAPSHOT value.
250     * If the variable is not set/empty it will default to use the timestamp.
251     */
252    @Parameter(defaultValue = "SNAPSHOT")
253    private String snapshotEnv;
254
255    /**
256     * Template for replacing the SNAPSHOT value. A timestamp format can be provided in brackets.
257     * prefix[yyMMdd]suffix -> prefix151230suffix
258     */
259    @Parameter
260    private String snapshotTemplate;
261
262    /**
263     * If verbose is true more build messages are logged.
264     */
265    @Parameter(defaultValue = "false")
266    private boolean verbose;
267
268    /**
269     * Indicates if the execution should be disabled. If <code>true</code>, nothing will occur during execution.
270     *
271     * @since 1.1
272     */
273    @Parameter(defaultValue = "false")
274    private boolean skip;
275
276    @Parameter(defaultValue = "true")
277    private boolean skipPOMs;
278
279    @Parameter(defaultValue = "false")
280    private boolean skipSubmodules;
281
282    /**
283     * @deprecated
284     */
285    @Parameter(defaultValue = "true")
286    private boolean submodules;
287
288
289    /**
290     * If signPackage is true then a origin signature will be placed
291     * in the generated package.
292     */
293    @Parameter(defaultValue = "false")
294    private boolean signPackage;
295
296    /**
297     * If signChanges is true then changes file will be signed.
298     */
299    @Parameter(defaultValue = "false")
300    private boolean signChanges;
301
302    /**
303     * Defines which utility is used to verify the signed package
304     */
305    @Parameter(defaultValue = "debsig-verify")
306    private String signMethod;
307
308    /**
309     * Defines the role to sign with
310     */
311    @Parameter(defaultValue = "origin")
312    private String signRole;
313
314    /**
315     * The keyring to use for signing operations.
316     */
317    @Parameter
318    private String keyring;
319
320    /**
321     * The key to use for signing operations.
322     */
323    @Parameter
324    private String key;
325
326    /**
327     * The passphrase to use for signing operations.
328     */
329    @Parameter
330    private String passphrase;
331
332    /**
333     * The prefix to use when reading signing variables
334     * from settings.
335     */
336    @Parameter(defaultValue = "jdeb.")
337    private String signCfgPrefix;
338
339    /**
340     * The settings.
341     */
342    @Parameter(defaultValue = "${settings}")
343    private Settings settings;
344
345    @Parameter(defaultValue = "")
346    private String propertyPrefix;
347
348    /**
349     * Sets the long file mode for the resulting tar file.  Valid values are "gnu", "posix", "error" or "truncate"
350     * @see org.apache.commons.compress.archivers.tar.TarArchiveOutputStream#setLongFileMode(int)
351     */
352    @Parameter(defaultValue = "gnu")
353    private String tarLongFileMode;
354
355    /**
356     * Sets the big number mode for the resulting tar file.  Valid values are "gnu", "posix" or "error"
357     * @see org.apache.commons.compress.archivers.tar.TarArchiveOutputStream#setBigNumberMode(int)
358     */
359    @Parameter(defaultValue = "gnu")
360    private String tarBigNumberMode;
361
362    /* end of parameters */
363
364    private static final String KEY = "key";
365    private static final String KEYRING = "keyring";
366    private static final String PASSPHRASE = "passphrase";
367
368    private String openReplaceToken = "[[";
369    private String closeReplaceToken = "]]";
370    private Console console;
371    private Collection<DataProducer> dataProducers = new ArrayList<DataProducer>();
372    private Collection<DataProducer> conffileProducers = new ArrayList<DataProducer>();
373
374    public void setOpenReplaceToken( String openReplaceToken ) {
375        this.openReplaceToken = openReplaceToken;
376    }
377
378    public void setCloseReplaceToken( String closeReplaceToken ) {
379        this.closeReplaceToken = closeReplaceToken;
380    }
381
382    protected void setData( Data[] dataSet ) {
383        this.dataSet = dataSet;
384        dataProducers.clear();
385        conffileProducers.clear();
386        if (dataSet != null) {
387            Collections.addAll(dataProducers, dataSet);
388
389            for (Data item : dataSet) {
390                if (item.getConffile()) {
391                    conffileProducers.add(item);
392                }
393            }
394        }
395    }
396
397    protected VariableResolver initializeVariableResolver( Map<String, String> variables ) {
398        @SuppressWarnings("unchecked")
399        final Map<String, String> projectProperties = Map.class.cast(getProject().getProperties());
400        @SuppressWarnings("unchecked")
401        final Map<String, String> systemProperties = Map.class.cast(System.getProperties());
402
403        variables.putAll(projectProperties);
404        variables.putAll(systemProperties);
405        variables.put("name", name != null ? name : getProject().getName());
406        variables.put("artifactId", getProject().getArtifactId());
407        variables.put("groupId", getProject().getGroupId());
408        variables.put("version", getProjectVersion());
409        variables.put("description", getProject().getDescription());
410        variables.put("extension", "deb");
411        variables.put("baseDir", getProject().getBasedir().getAbsolutePath());
412        variables.put("buildDir", buildDirectory.getAbsolutePath());
413        variables.put("project.version", getProject().getVersion());
414        variables.put("url", getProject().getUrl());
415
416        return new MapVariableResolver(variables);
417    }
418
419    /**
420     * Doc some cleanup and conversion on the Maven project version.
421     * <ul>
422     * <li>any "-" is replaced by "+"</li>
423     * <li>"SNAPSHOT" is replaced with the current time and date, prepended by "~"</li>
424     * </ul>
425     *
426     * @return the Maven project version
427     */
428    private String getProjectVersion() {
429        return Utils.convertToDebianVersion(getProject().getVersion(), this.snapshotExpand, this.snapshotEnv, this.snapshotTemplate, session.getStartTime());
430    }
431
432    /**
433     * @return whether the artifact is a POM or not
434     */
435    private boolean isPOM() {
436        String type = getProject().getArtifact().getType();
437        return "pom".equalsIgnoreCase(type);
438    }
439
440    /**
441     * @return whether the artifact is of configured type (i.e. the package to generate is the main artifact)
442     */
443    private boolean isType() {
444        return type.equals(getProject().getArtifact().getType());
445    }
446
447    /**
448     * @return whether or not Maven is currently operating in the execution root
449     */
450    private boolean isSubmodule() {
451        // FIXME there must be a better way
452        return !session.getExecutionRootDirectory().equalsIgnoreCase(baseDir.toString());
453    }
454
455    /**
456     * @return whether or not the main artifact was created
457     */
458    private boolean hasMainArtifact() {
459        final MavenProject project = getProject();
460        final Artifact artifact = project.getArtifact();
461        return artifact.getFile() != null && artifact.getFile().isFile();
462    }
463
464    /**
465     * Main entry point
466     *
467     * @throws MojoExecutionException on error
468     */
469    public void execute() throws MojoExecutionException {
470
471        final MavenProject project = getProject();
472
473        if (skip) {
474            getLog().info("skipping as configured (skip)");
475            return;
476        }
477
478        if (skipPOMs && isPOM()) {
479            getLog().info("skipping because artifact is a pom (skipPOMs)");
480            return;
481        }
482
483        if (skipSubmodules && isSubmodule()) {
484            getLog().info("skipping submodule (skipSubmodules)");
485            return;
486        }
487
488
489        setData(dataSet);
490
491        console = new MojoConsole(getLog(), verbose);
492
493        initializeSignProperties();
494
495        final VariableResolver resolver = initializeVariableResolver(new HashMap<String, String>());
496
497        final File debFile = new File(Utils.replaceVariables(resolver, deb, openReplaceToken, closeReplaceToken));
498        final File controlDirFile = new File(Utils.replaceVariables(resolver, controlDir, openReplaceToken, closeReplaceToken));
499        final File installDirFile = new File(Utils.replaceVariables(resolver, installDir, openReplaceToken, closeReplaceToken));
500        final File changesInFile = new File(Utils.replaceVariables(resolver, changesIn, openReplaceToken, closeReplaceToken));
501        final File changesOutFile = new File(Utils.replaceVariables(resolver, changesOut, openReplaceToken, closeReplaceToken));
502        final File changesSaveFile = new File(Utils.replaceVariables(resolver, changesSave, openReplaceToken, closeReplaceToken));
503        final File keyringFile = keyring == null ? null : new File(Utils.replaceVariables(resolver, keyring, openReplaceToken, closeReplaceToken));
504
505        // if there are no producers defined we try to use the artifacts
506        if (dataProducers.isEmpty()) {
507
508            if (hasMainArtifact()) {
509                Set<Artifact> artifacts = new HashSet<Artifact>();
510
511                artifacts.add(project.getArtifact());
512
513                @SuppressWarnings("unchecked")
514                final Set<Artifact> projectArtifacts = project.getArtifacts();
515
516                for (Artifact artifact : projectArtifacts) {
517                    artifacts.add(artifact);
518                }
519
520                @SuppressWarnings("unchecked")
521                final List<Artifact> attachedArtifacts = project.getAttachedArtifacts();
522
523                for (Artifact artifact : attachedArtifacts) {
524                    artifacts.add(artifact);
525                }
526
527                for (Artifact artifact : artifacts) {
528                    final File file = artifact.getFile();
529                    if (file != null) {
530                        dataProducers.add(new DataProducer() {
531                            public void produce( final DataConsumer receiver ) {
532                                try {
533                                    final File path = new File(installDirFile.getPath(), file.getName());
534                                    final String entryName = path.getPath();
535
536                                    final boolean symbolicLink = SymlinkUtils.isSymbolicLink(path);
537                                    final TarArchiveEntry e;
538                                    if (symbolicLink) {
539                                        e = new TarArchiveEntry(entryName, TarConstants.LF_SYMLINK);
540                                        e.setLinkName(SymlinkUtils.readSymbolicLink(path));
541                                    } else {
542                                        e = new TarArchiveEntry(entryName, true);
543                                    }
544
545                                    e.setUserId(0);
546                                    e.setGroupId(0);
547                                    e.setUserName("root");
548                                    e.setGroupName("root");
549                                    e.setMode(TarEntry.DEFAULT_FILE_MODE);
550                                    e.setSize(file.length());
551
552                                    receiver.onEachFile(new FileInputStream(file), e);
553                                } catch (Exception e) {
554                                    getLog().error(e);
555                                }
556                            }
557                        });
558                    } else {
559                        getLog().error("No file for artifact " + artifact);
560                    }
561                }
562            }
563        }
564
565        try {
566            DebMaker debMaker = new DebMaker(console, dataProducers, conffileProducers);
567            debMaker.setDeb(debFile);
568            debMaker.setControl(controlDirFile);
569            debMaker.setPackage(getProject().getArtifactId());
570            debMaker.setDescription(getProject().getDescription());
571            debMaker.setHomepage(getProject().getUrl());
572            debMaker.setChangesIn(changesInFile);
573            debMaker.setChangesOut(changesOutFile);
574            debMaker.setChangesSave(changesSaveFile);
575            debMaker.setCompression(compression);
576            debMaker.setKeyring(keyringFile);
577            debMaker.setKey(key);
578            debMaker.setPassphrase(passphrase);
579            debMaker.setSignPackage(signPackage);
580            debMaker.setSignChanges(signChanges);
581            debMaker.setSignMethod(signMethod);
582            debMaker.setSignRole(signRole);
583            debMaker.setResolver(resolver);
584            debMaker.setOpenReplaceToken(openReplaceToken);
585            debMaker.setCloseReplaceToken(closeReplaceToken);
586            debMaker.setDigest(digest);
587            debMaker.setTarBigNumberMode(tarBigNumberMode);
588            debMaker.setTarLongFileMode(tarLongFileMode);
589            debMaker.validate();
590            debMaker.makeDeb();
591
592            // Always attach unless explicitly set to false
593            if ("true".equalsIgnoreCase(attach)) {
594                console.info("Attaching created debian package " + debFile);
595                if (!isType()) {
596                    projectHelper.attachArtifact(project, type, classifier, debFile);
597                } else {
598                    project.getArtifact().setFile(debFile);
599                }
600            }
601
602        } catch (PackagingException e) {
603            getLog().error("Failed to create debian package " + debFile, e);
604            throw new MojoExecutionException("Failed to create debian package " + debFile, e);
605        }
606
607        if (!isBlank(propertyPrefix)) {
608          project.getProperties().put(propertyPrefix+"version", getProjectVersion() );
609          project.getProperties().put(propertyPrefix+"deb", debFile.getAbsolutePath());
610          project.getProperties().put(propertyPrefix+"deb.name", debFile.getName());
611          project.getProperties().put(propertyPrefix+"changes", changesOutFile.getAbsolutePath());
612          project.getProperties().put(propertyPrefix+"changes.name", changesOutFile.getName());
613          project.getProperties().put(propertyPrefix+"changes.txt", changesSaveFile.getAbsolutePath());
614          project.getProperties().put(propertyPrefix+"changes.txt.name", changesSaveFile.getName());
615        }
616
617    }
618
619    /**
620     * Initializes unspecified sign properties using available defaults
621     * and global settings.
622     */
623    private void initializeSignProperties() {
624        if (!signPackage && !signChanges) {
625            return;
626        }
627
628        if (key != null && keyring != null && passphrase != null) {
629            return;
630        }
631
632        Map<String, String> properties =
633                readPropertiesFromActiveProfiles(signCfgPrefix, KEY, KEYRING, PASSPHRASE);
634
635        key = lookupIfEmpty(key, properties, KEY);
636        keyring = lookupIfEmpty(keyring, properties, KEYRING);
637        passphrase = decrypt(lookupIfEmpty(passphrase, properties, PASSPHRASE));
638
639        if (keyring == null) {
640            try {
641                keyring = Utils.guessKeyRingFile().getAbsolutePath();
642                console.info("Located keyring at " + keyring);
643            } catch (FileNotFoundException e) {
644                console.warn(e.getMessage());
645            }
646        }
647    }
648
649    /**
650     * Decrypts given passphrase if needed using maven security dispatcher.
651     * See http://maven.apache.org/guides/mini/guide-encryption.html for details.
652     *
653     * @param maybeEncryptedPassphrase possibly encrypted passphrase
654     * @return decrypted passphrase
655     */
656    private String decrypt( final String maybeEncryptedPassphrase ) {
657        if (maybeEncryptedPassphrase == null) {
658            return null;
659        }
660
661        try {
662            final String decrypted = secDispatcher.decrypt(maybeEncryptedPassphrase);
663            if (maybeEncryptedPassphrase.equals(decrypted)) {
664                console.info("Passphrase was not encrypted");
665            } else {
666                console.info("Passphrase was successfully decrypted");
667            }
668            return decrypted;
669        } catch (SecDispatcherException e) {
670            console.warn("Unable to decrypt passphrase: " + e.getMessage());
671        }
672
673        return maybeEncryptedPassphrase;
674    }
675
676    /**
677     *
678     * @return the maven project used by this mojo
679     */
680    private MavenProject getProject() {
681        if (project.getExecutionProject() != null) {
682            return project.getExecutionProject();
683        }
684
685        return project;
686    }
687
688
689
690    /**
691     * Read properties from the active profiles.
692     *
693     * Goes through all active profiles (in the order the
694     * profiles are defined in settings.xml) and extracts
695     * the desired properties (if present). The prefix is
696     * used when looking up properties in the profile but
697     * not in the returned map.
698     *
699     * @param prefix The prefix to use or null if no prefix should be used
700     * @param properties The properties to read
701     *
702     * @return A map containing the values for the properties that were found
703     */
704    public Map<String, String> readPropertiesFromActiveProfiles( final String prefix,
705                                                                 final String... properties ) {
706        if (settings == null) {
707            console.debug("No maven setting injected");
708            return Collections.emptyMap();
709        }
710
711        final List<String> activeProfilesList = settings.getActiveProfiles();
712        if (activeProfilesList.isEmpty()) {
713            console.debug("No active profiles found");
714            return Collections.emptyMap();
715        }
716
717        final Map<String, String> map = new HashMap<String, String>();
718        final Set<String> activeProfiles = new HashSet<String>(activeProfilesList);
719
720        // Iterate over all active profiles in order
721        for (final Profile profile : settings.getProfiles()) {
722            // Check if the profile is active
723            final String profileId = profile.getId();
724            if (activeProfiles.contains(profileId)) {
725                console.debug("Trying active profile " + profileId);
726                for (final String property : properties) {
727                    final String propKey = prefix != null ? prefix + property : property;
728                    final String value = profile.getProperties().getProperty(propKey);
729                    if (value != null) {
730                        console.debug("Found property " + property + " in profile " + profileId);
731                        map.put(property, value);
732                    }
733                }
734            }
735        }
736
737        return map;
738    }
739
740}