7
votes

I'm working on automating the installer for a .NET 4.0 ClickOnce WPF application, which needs a few items to be set in the app.config file. I've gone through the thorny process of finding specific steps I must follow using Mage.exe (that is, update and re-sign application and deployment manifests) and now am trying to automate it for installation.

I opted to use .deploy extension to minimize issues with IIS/Internet Explorer security mechanisms, so essentially the algorithm is as follows (based on Signing and re-signing manifests in ClickOnce (Saurabh Bhatia) and Update Configuration of a ClickOnce WPF Application Using Mage or MageUI, as primary sources among others):

  1. Go to the \Application Files\App_%HighestVersion%\ folder
  2. Remove .deploy extension for files that have it
  3. Run  mage -u %app%.exe.manifest -cf cert.pfx
  4. Restore .deploy extension
  5. Run  mage -u %app%.application -appm %app%.exe.manifest -cf cert.pfx
  6. Copy %app%.application 2 levels up (to ..\.. - deployment root)

That works perfectly if done manually. I can run a .cmd file, customized for environment specifics (paths, etc.), but then I'd need to include mage.exe in the deployment, and whether Microsoft allows us to do that is an open question for me. Thus, I'm trying to perform similar actions in the Installer class:

X509Certificate2 ct = new X509Certificate2(sPathCert);

//  .. Remove .deploy extension (for files in the sPathApp folder).

sPathMft = Directory.GetFiles(sPathApp, "*.exe.manifest")[0];
ApplicationManifest am = ManifestReader.ReadManifest( "ApplicationManifest", sPathMft, false ) as ApplicationManifest;
if (am == null)
    throw new ArgumentNullException("AppManifest");
am.ResolveFiles();
am.UpdateFileInfo( );
ManifestWriter.WriteManifest(am, sPathMft);
SecurityUtilities.SignFile(ct, null, sPathMft);

//    .. Restore .deploy extensions to files touched above.

sPathMft = Directory.GetFiles(sPathApp, "*.application")[0];
DeployManifest dm = ManifestReader.ReadManifest("DeployManifest", sPathMft, false) as DeployManifest;
if (dm == null)
    throw new ArgumentNullException( "DplManifest" );
dm.ResolveFiles();
dm.UpdateFileInfo();
ManifestWriter.WriteManifest(dm, sPathMft);
SecurityUtilities.SignFile(ct, null, sPathMft);

File.Copy(sPathMft, sPathBin + "\\" + dm.AssemblyIdentity.Name, true);

Now, here's the kicker. Everything works perfectly with exception of step 5. When the application is downloaded to the user's machine there's an issue with the deployment manifest:

  • Deployment manifest is not semantically valid.
  • Deployment manifest is missing <compatibleFrameworks>.

Indeed, this section is no longer present (however, it was in the original %app%.application!). A similar outcome is described in ClickOnce - .NET 4.0 errors: "Deployment manifest is not semantically valid" and "Deployment manifest is missing <compatibleFrameworks>", but is a result of a different process (msbuild). This section is new (and required) for 4.0 manifests, so my only guess is that somehow when ManifestWriter persists changes to disk it does it in a 3.5 fashion? I triple checked that a correct library is used (C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework.NETFramework\v4.0\Microsoft.Build.Tasks.v4.0.dll). What gives?

In lieu of an answer so far I tried to add the missing section manually:

dm.CompatibleFrameworks.Clear(); // Unnecessary as dm.CompatibleFrameworks.Count == 0 indeed!
CompatibleFramework cf = new CompatibleFramework();
cf.Version= "4.0";
cf.SupportedRuntime = "4.0.30319";
cf.Profile= "Client";
dm.CompatibleFrameworks.Add(cf);
cf = new CompatibleFramework();
cf.Version = "4.0";
cf.SupportedRuntime = "4.0.30319";
cf.Profile = "Full";
dm.CompatibleFrameworks.Add(cf);

But that has no effect, no matter where I place this code, before dm.ResolveFiles( ), dm.UpdateFileInfo( ) or ManifestWriter.WriteManifest(..)!

My result is similar to Stack Overflow questions MageUI.exe removes compatibleFrameworks element or Why does Mage.exe not generate a compatibleFrameworks attribute? or MageUI.exe is not including a compatibleFrameworks element, but I'm not using mageui, mage or even msbuild at all!

What's going on?

2
As a workaround, have you tried to directly manipulate the manifest XML instead of using the APIs? It worked for us in the past - you get full control over the contents, do whatever you want, and then re-sign the file once you're done.David Airapetyan
Astrogator - is there any chance that you could share you scripts? I am about to start a very similar process. Thanks!Erick T
Hi @ErickT! Well, the algorithm is pretty much laid out here. This code is executed by msiexec.exe via a Custom Action, which i specify in the Visual Studio 2010 Setup project - there are no 'scripts' per se. Let me know, if that is not clear enough, and i'll try to expose more.Astrogator

2 Answers

9
votes

Figured it out myself. The culprit is ManifestReader.ReadManifest( "DeployManifest", sPathMft, true ).

MSDN says, [preserveStream argument] "specifies whether to preserve the input stream in the InputStream property of the resulting manifest object. Used by ManifestWriter to reconstitute input which is not represented in the object representation."

Wording aside, setting true is not enough by itself: dm.CompatibleFrameworks.Count will still be 0, but now the addition of CompatibleFramework items will have an effect!

For someone else in the same boat, I do that before dm.ResolveFiles( ):

if(  dm.CompatibleFrameworks.Count <= 0  )
{
    CompatibleFramework cf= new CompatibleFramework( );
    cf.Profile= "Client";       cf.Version= "4.0";      cf.SupportedRuntime=    "4.0.30319";
    dm.CompatibleFrameworks.Add( cf );              //  cf= new CompatibleFramework( );
    cf.Profile= "Full";     //  cf.Version= "4.0";      cf.SupportedRuntime=    "4.0.30319";
    dm.CompatibleFrameworks.Add( cf );              /// no need for separate object
}

@davidair, thanks for your suggestion!  Agreed, though I prefer to work with API objects (vs. XML).
Another alternative is to call mage (directly or from a .cmd file), as it seems that we are allowed to redistribute it.


I also added the following portion, which doesn't have an impact on the question itself, but may be quite important for anyone following the same path (/client is the deployment root, and can be customized):

dm.DeploymentUrl=   string.Format( "http://{0}/{1}/client/{1}.application",
                        Dns.GetHostName( ), Context.Parameters[ scTokVirtDir ] );
dm.UpdateMode=      UpdateMode.Background;
dm.UpdateUnit=      UpdateUnit.Weeks;
dm.UpdateInterval=  1;
dm.UpdateEnabled=   true;

2019-Oct-08
Just stumbled on an issue with app.manifest:
compatibility section with supportedOS elements was stripped out during deployment.

Same root cause; the line reading it should set preserveStream to true:

ApplicationManifest am = ManifestReader.ReadManifest( "ApplicationManifest", sPathMft, true ) as ApplicationManifest;
1
votes

I also needed to add CompatibleFrameworks. I also tried do add the CompatibleFrameworks like this (which does not work)

dm.CompatibleFrameworks.Add(...);

My solution was to set:

dm.TargetFrameworkMoniker = ".NETFramework,Version=v4.0";                 

After this the Manifest generation was correct.

Be careful If you set the TargetFrameworkMoniker before WriteManifest you have the <compatibleFrameworks> twice and your application file is corrupt. Here is my solution for this:

DeployManifest dm = ManifestReader.ReadManifest("DeployManifest", applicationFileName, false) as DeployManifest;
dm.ResolveFiles();
//doing stuff..
dm.UpdateFileInfo();
ManifestWriter.WriteManifest(dm, applicationFileName);
dm.TargetFrameworkMoniker = ".NETFramework,Version=v4.0";
ManifestWriter.WriteManifest(dm, applicationFileName);