XCodeVersionInfoMojo.java
/*
* #%L
* xcode-maven-plugin
* %%
* Copyright (C) 2012 SAP AG
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package com.sap.prd.mobile.ios.mios;
import static com.sap.prd.mobile.ios.mios.XCodeVersionUtil.checkVersions;
import static com.sap.prd.mobile.ios.mios.XCodeVersionUtil.getVersion;
import static com.sap.prd.mobile.ios.mios.XCodeVersionUtil.getXCodeVersionString;
import static java.lang.String.format;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.xml.bind.JAXBException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProjectHelper;
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.repository.RemoteRepository;
import org.xml.sax.SAXException;
import com.sap.prd.mobile.ios.mios.CodeSignManager.ExecResult;
import com.sap.prd.mobile.ios.mios.CodeSignManager.ExecutionResultVerificationException;
import com.sap.prd.mobile.ios.mios.versioninfo.v_1_2_2.Dependency;
/**
* Generates a <artifact-id>-<version>-version.xml for reproducibility reasons. This
* versions.xml contains information about the scm location and revision of the built project and
* all its dependencies. Expects a sync.info file in the root folder of the project as input.
*
*
* The sync.info file is a property file. If used with perforce it must contain the following entries:
* <code>
* <ul>
* <li> type=perforce
* <li> port=<The url of the perforce server>
* <li> depotpath=<The path synced on the perforce server>
* <li> changelist=<The changelist of the change that is being built>
* </ul>
* </code>
*
*
* If used with git it must contain the following entries:
*
* <code>
* <ul>
* <li> type=git
* <li> repo=<The git repository>
* <li> commitId=<The commitId of the change that is being built>
* </ul>
* </code>
*
* For git based projects the sync.info file can be created with the following code snipped executed before the xcode-maven-plugin is triggered.
*
* <pre>
* echo "type=git" > sync.info
* echo "repo=scm:git:$(git remote -v |awk '/fetch/ {print $2;}')" >> sync.info
* echo "commitId=$(git rev-parse HEAD)" >> sync.info
* </pre>
*
* @goal attach-version-info
* @requiresDependencyResolution
*/
public class XCodeVersionInfoMojo extends BuildContextAwareMojo
{
private final static String MIN_XCODE_VERSION_NO_STRICT_VERIFY = "6.0.0";
private final static boolean DEFAULT_NO_STRICT_VERIFY_FOR_OLD_XCODE = false;
private final static boolean DEFAULT_NO_STRICT_VERIFY_FOR_NEW_XCODE = true;
/**
* The code sign identity is used to select the provisioning profile (e.g.
* <code>iPhone Distribution</code>, <code>iPhone Developer</code>).
*
* @parameter expression="${xcode.codeSignIdentity}"
* @since 1.2.0
*/
protected String codeSignIdentity;
/**
* The code signing required is used to disable code signing when no
* developer provisioning certificate is available (e.g.
* <code>NO</code>, <code>YES</code>).
*
* @parameter expression="${xcode.codeSigningRequired}" default-value = "true"
* @since 1.14.1
*/
protected boolean codeSigningRequired;
/**
*
* @parameter default-value="${session}"
* @required
* @readonly
*/
protected MavenSession mavenSession;
/**
* The entry point to Aether, i.e. the component doing all the work.
*
* @component
*/
protected RepositorySystem repoSystem;
/**
* The current repository/network configuration of Maven.
*
* @parameter default-value="${repositorySystemSession}"
* @readonly
*/
protected RepositorySystemSession repoSession;
/**
* The project's remote repositories to use for the resolution of project dependencies.
*
* @parameter default-value="${project.remoteProjectRepositories}"
* @readonly
*/
protected List<RemoteRepository> projectRepos;
/**
* @component
*/
private MavenProjectHelper projectHelper;
/**
* @parameter expression="${sync.info.file}" default-value="sync.info"
*/
private String syncInfo;
/**
* If <code>true</code> the build fails if it does not find a sync.info file in the root directory
*
* @parameter expression="${xcode.failOnMissingSyncInfo}" default-value="false"
*/
private boolean failOnMissingSyncInfo;
/**
* If <code>true</code> the codesign --verify will be called with --no-strict option
*
* @parameter expression="${xcode.noStrictVerify}"
*/
private String noStrictVerify;
/**
* If <code>true</code> confidential information is removed from artifacts to be released.
*
* @parameter expression="${xcode.hideConfidentialInformation}" default-value="true"
*/
private boolean hideConfidentialInformation;
@Override
public void execute() throws MojoExecutionException, MojoFailureException
{
final File syncInfoFile = new File(mavenSession.getExecutionRootDirectory(), syncInfo);
if (!syncInfoFile.exists()) {
if (failOnMissingSyncInfo)
{
throw new MojoExecutionException("Sync info file '" + syncInfoFile.getAbsolutePath()
+ "' not found. Please configure your SCM plugin accordingly.");
}
getLog().info("The optional sync info file '" + syncInfoFile.getAbsolutePath()
+ "' not found. Cannot attach versions.xml to build results.");
return;
}
getLog().info("Sync info file found: '" + syncInfoFile.getAbsolutePath() + "'. Creating versions.xml file.");
final File versionsXmlFile = new File(project.getBuild().getDirectory(), "versions.xml");
FileOutputStream os = null;
try {
os = new FileOutputStream(versionsXmlFile);
new VersionInfoXmlManager().createVersionInfoFile(project.getGroupId(), project.getArtifactId(),
project.getVersion(), syncInfoFile, getDependencies(), os);
}
catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
finally {
IOUtils.closeQuietly(os);
}
final File versionsPlistFile = new File(project.getBuild().getDirectory(), "versions.plist");
if (versionsPlistFile.exists()) {
if(!versionsPlistFile.delete())
{
throw new IllegalStateException(String.format("Cannot delete already existing plist file (%s)", versionsPlistFile));
}
}
try {
new VersionInfoPListManager().createVersionInfoPlistFile(project.getGroupId(), project.getArtifactId(),
project.getVersion(), syncInfoFile, getDependencies(), versionsPlistFile, hideConfidentialInformation);
}
catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
try {
if (PackagingType.getByMavenType(packaging) == PackagingType.APP)
{
try
{
copyVersionsFilesAndSign();
}
catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
catch (ExecutionResultVerificationException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
catch (XCodeException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
}
catch (PackagingType.UnknownPackagingTypeException ex) {
getLog().warn("Unknown packaing type detected.", ex);
}
projectHelper.attachArtifact(project, "xml", "versions", versionsXmlFile);
getLog().info("versions.xml '" + versionsXmlFile + " attached as additional artifact.");
}
private void copyVersionsFilesAndSign() throws IOException, ExecutionResultVerificationException, XCodeException,
MojoExecutionException
{
for (final String configuration : getConfigurations())
{
for (final String sdk : getSDKs())
{
if (sdk.startsWith("iphoneos"))
{
File versionsXmlInBuild = new File(project.getBuild().getDirectory(), "versions.xml");
File versionsPListInBuild = new File(project.getBuild().getDirectory(), "versions.plist");
File rootDir = XCodeBuildLayout.getAppFolder(getXCodeCompileDirectory(), configuration, sdk);
String productName = getProductName(configuration, sdk);
File appFolder = new File(rootDir, productName + ".app");
File versionsXmlInApp = new File(appFolder, "versions.xml");
File versionsPListInApp = new File(appFolder, "versions.plist");
boolean isCodeSignActive = true;
if((codeSignIdentity == null ||codeSignIdentity.isEmpty()) && !codeSigningRequired )
isCodeSignActive = false;
ExecResult originalCodesignEntitlementsInfo = null;
ExecResult originalSecurityCMSMessageInfo = null;
if(isCodeSignActive)
{
CodeSignManager.verify(appFolder, defineNoStrictVerifyBasedOnXcodeVersion());
originalCodesignEntitlementsInfo = CodeSignManager
.getCodesignEntitlementsInformation(appFolder);
originalSecurityCMSMessageInfo = CodeSignManager.getSecurityCMSInformation(appFolder);
}else{
getLog().info("CODE_SIGNING_REQUIRED=\"NO\"");
getLog().info("value of codeSignIdentity is "+codeSignIdentity);
getLog().info("value of codeSigningRequired is "+codeSigningRequired);
}
try {
if (hideConfidentialInformation) {
transformVersionsXml(versionsXmlInBuild, versionsXmlInApp);
}
else {
FileUtils.copyFile(versionsXmlInBuild, versionsXmlInApp);
}
}
catch (Exception e) {
throw new MojoExecutionException("Could not transform versions.xml: " + e.getMessage(), e);
}
getLog().info("Versions.xml file copied from: '" + versionsXmlInBuild + " ' to ' " + versionsXmlInApp);
FileUtils.copyFile(versionsPListInBuild, versionsPListInApp);
getLog().info("Versions.plist file copied from: '" + versionsPListInBuild + " ' to ' " + versionsPListInApp);
if(isCodeSignActive)
{
sign(rootDir, configuration, sdk);
final ExecResult resignedCodesignEntitlementsInfo = CodeSignManager
.getCodesignEntitlementsInformation(appFolder);
final ExecResult resignedSecurityCMSMessageInfo = CodeSignManager.getSecurityCMSInformation(appFolder);
CodeSignManager.verify(appFolder, defineNoStrictVerifyBasedOnXcodeVersion());
CodeSignManager.verify(originalCodesignEntitlementsInfo, resignedCodesignEntitlementsInfo);
CodeSignManager.verify(originalSecurityCMSMessageInfo, resignedSecurityCMSMessageInfo);
}
}
}
}
}
private boolean defineNoStrictVerifyBasedOnXcodeVersion() throws XCodeException
{
if (noStrictVerify != null && (noStrictVerify.equalsIgnoreCase("true") || noStrictVerify.equalsIgnoreCase("false"))) {
return Boolean.parseBoolean(noStrictVerify);
} else {
String xCodeVersionString = getXCodeVersionString();
DefaultArtifactVersion version = getVersion(xCodeVersionString);
if(checkVersions(version, MIN_XCODE_VERSION_NO_STRICT_VERIFY)) {
return DEFAULT_NO_STRICT_VERIFY_FOR_NEW_XCODE;
} else {
return DEFAULT_NO_STRICT_VERIFY_FOR_OLD_XCODE;
}
}
}
void transformVersionsXml(File versionsXmlInBuild, File versionsXmlInApp)
throws ParserConfigurationException, SAXException, IOException, TransformerFactoryConfigurationError,
TransformerException, XCodeException
{
final InputStream transformerRule = getClass().getClassLoader().getResourceAsStream(
"versionInfoCensorTransformation.xml");
if (transformerRule == null)
{
throw new XCodeException("Could not read transformer rule.");
}
try
{
final Transformer transformer = TransformerFactory.newInstance().newTransformer(
new StreamSource(transformerRule));
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
transformer.transform(new StreamSource(versionsXmlInBuild), new StreamResult(versionsXmlInApp));
}
finally {
IOUtils.closeQuietly(transformerRule);
}
}
private void sign(File rootDir, String configuration, String sdk) throws IOException, XCodeException
{
String csi = (codeSignIdentity !=null && codeSignIdentity.trim().length() > 0)? codeSignIdentity: EffectiveBuildSettings.getBuildSetting(
getXCodeContext(XCodeContext.SourceCodeLocation.WORKING_COPY, configuration, sdk),
EffectiveBuildSettings.CODE_SIGN_IDENTITY);
File appFolder = new File(EffectiveBuildSettings.getBuildSetting(
getXCodeContext(XCodeContext.SourceCodeLocation.WORKING_COPY, configuration, sdk),
EffectiveBuildSettings.CODESIGNING_FOLDER_PATH));
CodeSignManager.sign(csi, appFolder, true);
getLog().info("value of codeSignIdentity choosed for resign app: "+EffectiveBuildSettings.CODE_SIGN_IDENTITY);
getLog().info("value of codeSigningRequired during resign "+codeSigningRequired);
}
private List<Dependency> getDependencies() throws IOException
{
List<Dependency> result = new ArrayList<Dependency>();
for (@SuppressWarnings("rawtypes")
final Iterator it = project.getDependencyArtifacts().iterator(); it.hasNext();) {
final Artifact mainArtifact = (Artifact) it.next();
try {
org.sonatype.aether.artifact.Artifact sideArtifact = new XCodeDownloadManager(projectRepos, repoSystem,
repoSession).resolveSideArtifact(mainArtifact, "versions", "xml");
getLog().info("Version information retrieved for artifact: " + mainArtifact);
addParsedVersionsXmlDependency(result, sideArtifact);
}
catch (SideArtifactNotFoundException e) {
getLog().warn("Could not retrieve version information for artifact:" + mainArtifact);
}
}
return result;
}
void addParsedVersionsXmlDependency(List<Dependency> result,
org.sonatype.aether.artifact.Artifact sideArtifact) throws IOException
{
try {
result.add(VersionInfoXmlManager.parseDependency(sideArtifact.getFile()));
}
catch (SAXException e) {
getLog().warn(format(
"Version file '%s' for artifact '%s' contains invalid content (Non parsable XML). Ignoring this file.",
(sideArtifact.getFile() != null ? sideArtifact.getFile() : "<n/a>"), sideArtifact));
}
catch (JAXBException e) {
getLog().warn(format(
"Version file '%s' for artifact '%s' contains invalid content (Scheme violation). Ignoring this file.",
(sideArtifact.getFile() != null ? sideArtifact.getFile() : "<n/a>"), sideArtifact));
}
}
}