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>**/.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}