0
votes

We are building a Xamarin.Forms app targeting iOS and UWP. We have a requirement that the app should be available as separate installation for Staging and Production. Basically each environment has its own settings (server URLs, app configurations and so on), which are stored in an external Configuration.json file. The main point is that in order to dynamically pick a correct Configuration.json file for each environment, the app should not be re-built. We should build the app only once, and only change configurations. We were able to solve this problem for iOS

To support dynamic configurations for iOS, we did the following

  1. store all environment configuration files within the project like StagingConfiguration.json, ProductionConfiguration.json.
  2. Register separate application in iTunes connect per environment with its own bundle ID - e.g. com.mycompany.app.staging for staging, com.mycompany.app for production
  3. After the app is built and ipa file is generated, we used fastlane to resign the app with new bundle id and provisioning profile, something like this (we use Azure DevOps CI/CD pipeline for builds and release, this step is command line execution in the release pipeline, and references environment variables to do its job)
fastlane run  resign ipa:"PATH_TO.ipa" signing_identity:"$(SigningIdentity)" bundle_id:$(BundleId) provisioning_profile:"$(provisioningProfile.secureFilePath)"
  1. And then simply upload the resigned pipeline to AppStore
  2. Within the app, we detect what's the bundle ID of the app, and based on that pick the correct configuration file

So, we are trying to find similar solution for UWP as well. The output package of the UWP app is a package like this QQPad.Mobile.UWP_0.39.0.0_x86_x64_arm_bundle.appxupload. Probably, we will need to do one of the following to support our scenario

  • Do the same as in iOS - create separate UWP app in the store, store multiple config files within the project, change the app's package name after the appxupload project is ready, and on the code side, detect the bundle ID and choose the right configuration file
  • Or, if possible, even better solution would be the following
    1. Do not create separate app, as it's cumbersome to manage, but rather create separate package flights within the same app hosting
    2. Instead of changing package name, be able to locate the configuration file within the appxupload and replace it with the config file of our choice
    3. Repack the app and resign it
    4. Upload it to the correct package flight

Again, the main point is being able to dynamic pick or change the configurations without having to rebuild the app. Are there any utilities for UWP that are equivalent of fastlane for iOS, which would also allow app bundle manipulations? Or I will have to do this manually? If so, how exactly?

1
You can use a public api and return different result to dynamically pick a correct Configuration.json file.Jack Hua
@JackHua-MSFT thanks for the comment. That's one of the options I considered, but that adds extra complexity unnecessarily. But, we could use that if that's the last resort. However, the question still remains, how will the API understand whether the app is dev, staging or prod? The UWP app should still have some post-build configuration that will be readable from code and be sent to the API. So, if we could do that, we wouldn't need API at all and I would simply pick the right configuration locally, like I did for iOSkyurkchyan
how will the API understand whether the app is dev, staging or prod? I think there should be a parameter there when posting the api.Jack Hua
Yes, that's right. My question was not how I will send that parameter to the API, but from where I will read that parameter value. We are building the app only once, which means the app should somehow know whether it's being run on dev, staging or production environments. Thus, there should be some post-built change in the package of the app, that will differentiate dev/staging/production deployments. And what I am trying to say, is that if we have that differentiation and if we are able to read that value in code, we woudln't need API, but would pick the right configuration within the app.kyurkchyan
Ok, you are right. It should be something like bundle ID in iOS. Is there anything like the name but you don't use in UWP? If you don't use it, you can set different values like bundle Id in iOS and use that the check environment.Jack Hua

1 Answers

0
votes

I have managed to resolve this after a lot of trials and failures.

I've posted the script that does the necessary transformations IN HERE

Here are the steps to summarize the basic strategy

  1. The script accepts the environment, appxupload path, the signing certificate and the password
  2. First thing we do is extract appxupload archive. For that, we need to copy the appxupload file with *.zip extension instead, so that it'll be valid input for Expand-Archive powershell utility
  3. Inside the extracted archive we then have appxbundle. For this one and for appx packages as well there's nice utility to pack/unpack them called MakeAppx. First we use this tool to unpack the appxbundle.
  4. Inside the unpacked folder we will find appx package files for different architectures and scales. We will have to unpack each and every one of them and make necessary changes to AppxManifest.xml file.
  5. AppxManifest.xml file is then opened as xml object, we find the PackageName attribute, and replace it with something like CURRENT_PACKAGE_NAME - STAGING for staging environment.
  6. Afterwards, we will have to repack and sign each appx package by replacing the old packages
  7. Then, after all appx packages are re-packed, we can pack the appxbundle by using MakeAppx utility. We will have to sign this one as well
  8. Then we need to recreate the appxupload archive by using Compress-Archive powershell utility. This type of bundle doesn't require signing.
  9. YEAAAH, we are ready to push the appxupload to UWP store!

The above steps were the complex ones. In order to read the package name and extract the name of the environment inside the UWP runtime I had to write couple of lines of code

var name = Windows.ApplicationModel.Package.Current.DisplayName;
string[] parts = name.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 1) return "Production";

string environmentName = parts.Last().Trim().ToLower();

I assume that if the environment specifier is not there after the "-" it's production.

The rest of the configuration is already up to you do decide. Personally I have used Microsoft.Extensions.Configuration with appsettings.json files embedded into the assembly as EmbeddedResources

var configs = new ConfigurationBuilder();
var fileProvider = new EmbeddedFileProvider(typeof(Bootstrapper).Assembly, typeof(Startup).Namespace);
configs.AddJsonFile(fileProvider, "appsettings.json", false, false)
        .AddJsonFile(fileProvider, $"appsettings.{environmentName.ToLower()}.json", true, false);

UPDATE

Unfortunately I had to abandon this idea... Why I thought that it worked in the first place, is because I only executed the script and uploaded it to the STAGING flight. After that, our CI pipeline, once QA was done testing, tried to upload the original not-modified appxupload file to the production Alpha Testing flight, and that's where problems arose and I received errors like this one

InvalidParameterValue: Appxbundles (including previously published and currently uploaded) must be uniquely identified by their full names. You have provided two packages with the full name ****_0.55.3.0_X86_ which have different contents. Please remove one of these packages, or increment current package versions to continue.

It seems that portal can't accept the same package, with the same identity name and same version numbers.

I thought I could update the appx file names as well and append STAGING to all files were necessary (see the script, I've updated it with these changes), but that didn't resolve the issue either.

Eventually, I decided to abandon the trials as it was taking too much time and roll with the multiple builds approach, and rebuild the app for each environment separately, and deploy the STAGING version not into the same app, but created totally separate app that hosts the STAGING version.