Updating a ClickOnce manifest using MSBuild

by Michael F. Collins, III March 29, 2009 09:32

In an earlier post, I discussed how to use MSBuild to publish a ClickOnce deployment package, and then how to modify that package to add additional files. After working with it some more, I’ve gone back and revised my custom MSBuild task to do more than just add files. I noticed the following shortcomings that I’ve fixed:

  • I mistakenly added non-binary files as data files. This was incorrect to do. ClickOnce applications have a special data directory that they are given when they are installed. Files marked as data files are copied to this data directory and not stored in the main program directory. I added a new DataFile metdata attribute to files that will let you mark a file as a data file.
  • ClickOnce recommends that file names have “.deploy” appended to them for security reasons. My MSBuild task wasn’t appending “.deploy” to the ends of the file names. I corrected this.

The new feature that I added to the MSBuild task is being able to change the product information. The name of the publisher, product suite, product title, support URL, and error reporting URL are now settable using MSBuild. But why would you want to do this?

When I build my software projects, I end up building both by Debug and Release builds. The release builds are the builds that I hope to someday send out to end users to play with. The Debug builds are the builds that I want to release for QA and testing. However, I want my testing users to be able to have both the QA and production release builds installed. By changing the product information in the ClickOnce manifest, my users can have both installed.

For example, in a current project that I’m developing named ImaginaryFinance that I’m hoping to commercialize at some point, I have the debug and release builds. The release build has the suite name set to “ImaginaryFinance” and the product name set to “ImaginaryFinance.” Meanwhile, the debug build has the suite name set to “ImaginaryFinance (QA)”, and the product name is set to “ImaginaryFinance (QA).” This way, both versions can be installed concurrently, and the user can tell the difference between the two.

Also, to support the concurrent installations, I’ve made the deployment URL in the deployment manifest settable using the MSBuild task as well.

Since I’m now doing more than just adding files to the manifest, I’ve renamed my custom build task to UpdateClickOnceManifest. Here’s the source code:

namespace ImaginaryRealities.Finance.MSBuild
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Security.Cryptography.X509Certificates;
    using Microsoft.Build.Framework;
    using Microsoft.Build.Tasks.Deployment.ManifestUtilities;
    using Microsoft.Build.Utilities;

    /// 
    /// Updates a ClickOnce manifest by changing the product information
    /// and adding files to the manifest, then re-signs the manifest.
    /// 
    public class UpdateClickOnceManifest : Task
    {
        /// 
        /// The  object for the application.
        /// 
        private ApplicationManifest applicationManifest;

        /// 
        /// The path to the ClickOnce package.
        /// 
        private string applicationPath;

        /// 
        /// The  object for the application.
        /// 
        private DeployManifest deploymentManifest;

        /// 
        /// Gets or sets the manifest file to modify.
        /// 
        /// 
        /// The value of this property is the path to the application
        /// manifest to be updated.
        /// 
        [Required]
        public string ApplicationManifestFile
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the path to the certificate to use to sign the
        /// ClickOnce manifest.
        /// 
        /// 
        /// The value of this property is the path to the X.509 certificate
        /// that is used to sign the manifests.
        /// 
        [Required]
        public string CertificatePath
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the password for the certificate used to sign the
        /// ClickOnce manifest.
        /// 
        /// 
        /// The value of this property is the password for the X.509
        /// certificate.
        /// 
        public string CertificatePassword
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the URL where new versions of the application will
        /// be deployed for users to upgrade.
        /// 
        /// 
        /// The value of this property is the URL where new versions of the
        /// application will be deployed for users to upgrade.
        /// 
        [SuppressMessage(
            "Microsoft.Design",
            "CA1056:UriPropertiesShouldNotBeStrings",
            Justification = "MSBuild only supports strings, not URIs.")]
        public string DeploymentUrl
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the URL to use for reporting application errors.
        /// 
        /// 
        /// The value of this property is a URL that the user will be directed
        /// to in order to report application errors.
        /// 
        [SuppressMessage(
            "Microsoft.Design",
            "CA1056:UriPropertiesShouldNotBeStrings",
            Justification = "MSBuild only supports strings, not URIs.")]
        public string ErrorReportUrl
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the list of files to add to the manifest.
        /// 
        /// 
        /// The value of this property is an array of 
        /// objects containing the list of files to add to the manifest and
        /// their relative location in metadata.
        /// 
        public ITaskItem[] FilesToAdd
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the title of the product that will be deployed using
        /// ClickOnce.
        /// 
        /// 
        /// The value of this property is the updated title of the software
        /// product.
        /// 
        public string Product
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the name of the publishers that is producing the
        /// software product.
        /// 
        /// 
        /// The value of this property is the updated title of the software
        /// publisher.
        /// 
        public string Publisher
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the name of the product suite.
        /// 
        /// 
        /// The value of this property is the name of the product suite.
        /// 
        public string SuiteName
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the support URL for the software product.
        /// 
        /// 
        /// The value of this property is the updated support URL for the
        /// software product.
        /// 
        [SuppressMessage(
            "Microsoft.Design",
            "CA1056:UriPropertiesShouldNotBeStrings",
            Justification = "MSBuild only supports strings, not URIs.")]
        public string SupportUrl
        {
            get;
            set;
        }

        /// 
        /// Gets or sets the URL of the secure timestamp server.
        /// 
        /// 
        /// The value of this property is the URL of the secure timestamp
        /// server.
        /// 
        [SuppressMessage(
            "Microsoft.Design",
            "CA1056:UriPropertiesShouldNotBeStrings",
            Justification = "This property needs to be a string for MSBuild.")]
        public string TimestampUrl
        {
            get;
            set;
        }

        /// 
        /// Adds the files to the manifest and re-signs the manifest.
        /// 
        /// 
        /// Returns true if the files were added to the manifest and the
        /// manifest was signed, or false if an error occurred.
        /// 
        public override bool Execute()
        {
            try
            {
                this.LoadDeploymentManifest();
                this.LoadAssemblyManifest();
                this.AddFilesToApplicationManifest();
                this.UpdateDescription();
                this.SignManifests();
                this.RenameFiles();
            }
            catch (ArgumentException e)
            {
                this.Log.LogErrorFromException(e);
                return false;
            }
            catch (FileNotFoundException e)
            {
                this.Log.LogErrorFromException(e);
                return false;
            }

            return true;
        }

        /// 
        /// Adds the additional files to the application manifest.
        /// 
        private void AddFilesToApplicationManifest()
        {
            if (null == this.FilesToAdd)
            {
                return;
            }

            foreach (var file in this.FilesToAdd)
            {
                var targetFolder = file.GetMetadata("TargetFolder") ??
                    String.Empty;
                var targetPath =
                    Path.Combine(this.applicationPath, targetFolder);
                if (!Directory.Exists(targetPath))
                {
                    Directory.CreateDirectory(targetPath);
                }

                var destinationFilename = Path.Combine(
                    targetPath,
                    Path.GetFileName(file.ItemSpec));
                File.Copy(file.ItemSpec, destinationFilename, true);

                var identity = AssemblyIdentity.FromFile(destinationFilename);
                BaseReference fileReference;
                if (null != identity)
                {
                    fileReference =
                        this.applicationManifest.AssemblyReferences.Add(
                            destinationFilename);
                }
                else
                {
                    fileReference =
                        this.applicationManifest.FileReferences.Add(
                            destinationFilename);
                    var isDataFile = file.GetMetadata("DataFile");
                    if (!String.IsNullOrEmpty(isDataFile))
                    {
                        var value = Boolean.Parse(isDataFile);
                        if (value)
                        {
                            ((FileReference)fileReference).IsDataFile = true;
                        }
                    }
                }

                if (!String.IsNullOrEmpty(targetFolder))
                {
                    fileReference.TargetPath = Path.Combine(
                        targetFolder,
                        Path.GetFileName(destinationFilename));
                }

                var group = file.GetMetadata("Group");
                if (!String.IsNullOrEmpty(group))
                {
                    fileReference.Group = group;
                }

                var optional = file.GetMetadata("IsOptional");
                if (!String.IsNullOrEmpty(optional) && "true" == optional)
                {
                    fileReference.IsOptional = true;
                }
            }
        }

        /// 
        /// Loads the application manifest.
        /// 
        /// 
        /// The application manifest file was not found.
        /// 
        private void LoadAssemblyManifest()
        {
            var assemblyManifestPath =
                this.deploymentManifest.EntryPoint.TargetPath;
            assemblyManifestPath = Path.Combine(
                Path.GetDirectoryName(this.deploymentManifest.SourcePath),
                assemblyManifestPath);
            this.applicationPath = Path.GetDirectoryName(assemblyManifestPath);

            var manifest =
                ManifestReader.ReadManifest(assemblyManifestPath, false);
            this.applicationManifest = manifest as ApplicationManifest;
            if (null == this.applicationManifest)
            {
                throw new FileNotFoundException(
                    "Unable to open referenced application manifest",
                    assemblyManifestPath);
            }
        }

        /// 
        /// Loads the deployment manifest for the application.
        /// 
        /// 
        /// The deployment manifest is not a valid deployment manifest.
        /// 
        private void LoadDeploymentManifest()
        {
            var manifest = ManifestReader.ReadManifest(
                this.ApplicationManifestFile,
                false);
            this.deploymentManifest = manifest as DeployManifest;
            if (null == this.deploymentManifest)
            {
                throw new ArgumentException("Not a valid deployment manifest");
            }
        }

        /// 
        /// Renames the added files to add the .deploy extension to the
        /// file names.
        /// 
        private void RenameFiles()
        {
            if (null == this.FilesToAdd)
            {
                return;
            }

            foreach (var file in this.FilesToAdd)
            {
                var targetFolder = file.GetMetadata("TargetFolder") ??
                    String.Empty;
                var targetPath =
                    Path.Combine(this.applicationPath, targetFolder);
                var destinationFilename = Path.Combine(
                    targetPath,
                    Path.GetFileName(file.ItemSpec));
                File.Move(
                    destinationFilename,
                    destinationFilename + ".deploy");
            }
        }

        /// 
        /// Signs the application and deployment manifest using the specified
        /// certificate.
        /// 
        /// 
        /// The certificate was not found.
        /// 
        private void SignManifests()
        {
            if (String.IsNullOrEmpty(this.CertificatePath))
            {
                return;
            }

            if (!File.Exists(this.CertificatePath))
            {
                throw new FileNotFoundException(
                    "Invalid certificate file path",
                    this.CertificatePath);
            }

            var certificate = String.IsNullOrEmpty(this.CertificatePassword)
                ? new X509Certificate2(this.CertificatePath)
                : new X509Certificate2(
                    this.CertificatePath,
                    this.CertificatePassword);

            var timestampUri = String.IsNullOrEmpty(this.TimestampUrl)
                ? null
                : new Uri(this.TimestampUrl);

            this.applicationManifest.ResolveFiles();
            this.applicationManifest.UpdateFileInfo();
            ManifestWriter.WriteManifest(this.applicationManifest);
            SecurityUtilities.SignFile(
                certificate,
                timestampUri,
                this.applicationManifest.SourcePath);

            ManifestWriter.WriteManifest(this.deploymentManifest);
            SecurityUtilities.SignFile(
                certificate,
                timestampUri,
                this.deploymentManifest.SourcePath);
        }

        /// 
        /// Updates the description of the application in the manifest.
        /// 
        [SuppressMessage(
            "Microsoft.Portability",
            "CA1903:UseOnlyApiFromTargetedFramework",
            MessageId = 
"Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest
.#set_SuiteName(System.String)",
            Justification = 
"The target framework for this component is .NET 3.5 SP1.")]
        [SuppressMessage(
            "Microsoft.Portability",
            "CA1903:UseOnlyApiFromTargetedFramework",
            MessageId = 
"Microsoft.Build.Tasks.Deployment.ManifestUtilities.ApplicationManifest
.#set_ErrorReportUrl(System.String)",
            Justification =
 "The target framework for this component is .NET 3.5 SP1.")]
        [SuppressMessage(
            "Microsoft.Portability",
            "CA1903:UseOnlyApiFromTargetedFramework",
            MessageId =
 "Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest.
#set_SuiteName(System.String)",
            Justification = 
"The target framework for this component is .NET 3.5 SP1.")]
        [SuppressMessage(
            "Microsoft.Portability",
            "CA1903:UseOnlyApiFromTargetedFramework",
            MessageId = 
"Microsoft.Build.Tasks.Deployment.ManifestUtilities.DeployManifest
.#set_ErrorReportUrl(System.String)",
            Justification = 
"The target framework for this component is .NET 3.5 SP1.")]
        private void UpdateDescription()
        {
            if (null != this.Publisher)
            {
                this.applicationManifest.Publisher = this.Publisher;
                this.deploymentManifest.Publisher = this.Publisher;
            }

            if (null != this.SuiteName)
            {
                this.applicationManifest.SuiteName = this.SuiteName;
                this.deploymentManifest.SuiteName = this.SuiteName;
            }

            if (null != this.Product)
            {
                this.applicationManifest.Product = this.Product;
                this.deploymentManifest.Product = this.Product;
            }

            if (null != this.SupportUrl)
            {
                this.applicationManifest.SupportUrl = this.SupportUrl;
                this.deploymentManifest.SupportUrl = this.SupportUrl;
            }

            if (null != this.ErrorReportUrl)
            {
                this.applicationManifest.ErrorReportUrl = this.ErrorReportUrl;
                this.deploymentManifest.ErrorReportUrl = this.ErrorReportUrl;
            }

            if (null != this.DeploymentUrl)
            {
                this.deploymentManifest.DeploymentUrl = this.DeploymentUrl;
            }
        }
    }
}


Tags: ,

ClickOnce | MSBuild

Comments

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen | Modified by Mooglegiant

Calendar

<<  March 2010  >>
MoTuWeThFrSaSu
22232425262728
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar

What I'm reading now


Add to Technorati Favorites

Disclaimer

The views expressed on this website/blog are the opinions of Michael F. Collins, III, and do not necessarily reflect the views of my employer.