Back to Blog Home
← all posts

Developing a NativeScript app for iOS when you don't have a Mac

January 18, 2022 — by Jason Cassidy

It is obviously preferable to have access to a Mac for iOS development, but if you wish to try your app on iOS and only have access to a windows machine then this blog post will show you one way of accomplishing this.

This guide will use the free version of Azure Devops to build our project for iOS, which we will then deploy to our iPhone.

Setup the required Apple Certificates / Profiles

You will need to enroll in the Apple Developer Program.

Create a development certificate

Once you have enrolled the next step is to create a development certificate.

  • Login to your apple developer account
  • Choose Certificates, IDs & Profiles
  • Select Certificates
  • Click the + to add a new certificate.
  • Choose Apple Development, Continue

Now apple are requesting a CSR, for this will will need some tools for windows.

  • Download & install openssl 1.X ( Note: openssl do not distribute binaries so you will have to visit one of the third party sites listed on that page. )

  • Generate a private key

    openssl genrsa -out mykey.key 2048
    
  • Generate a CSR

    openssl req -new -key mykey.key -out CertificateSigning.csr -subj "/[email protected], CN=Jason Cassidy, C=IE"
    
  • Upload the CSR to apple and download the generated certificate

  • Copy the certificate to the same folder where we generated the CSR and Key.

  • Create the .p12 ( combine the private key and certificate )

    Convert to PEM format:

    openssl x509 -in development.cer -inform DER -out development.pem -outform PEM
    

    Generate the .p12

      openssl pkcs12 -export -inkey mykey.key -in development.pem -out development.p12 -legacy
    

This will require that you enter a password, remember this as we will need it later.

Register your Device with Apple

  • Login to your apple developer account

  • Choose Certificates, IDs & Profiles

  • Choose Devices and click the + icon

  • Enter a Device Name

  • You now need to get your Device Id, run the following command with your iPhone plugged into your PC. ( This may only work if you have iTunes installed )

     ns device
    

    The Device Identifier listed for your phone is the Device ID (UDID), which you should enter.

  • Click Continue through the remaining screens.

Create an App Identifier

  • Login to your apple developer account
  • Choose Certificates, IDs & Profiles
  • Choose Identifiers and click the + icon
  • Select App IDs and click Continue
  • Choose Type App and click Continue
  • Enter a Description and an Explicit Bundle ID, e.g. my.company.testApp
  • Click Continue
  • Click Register

Create a Profile

  • Login to your apple developer account
  • Choose Certificates, IDs & Profiles
  • Choose Profiles and click the + icon
  • Choose iOS App Development and click Continue
  • Select the App ID that you created in the previous step (e.g. my.company.testApp) and click Continue
  • Choose the Certificate you just created and click Continue
  • Choose the Device you have previously registered
  • Give the profile a Name and click Generate
  • You can now download the Profile

Create our GitHub project

  • Create a sample NativeScript app of the flavour of your choice.

    i.e.

    ns create
    
  • Edit nativescript.config.ts changing the id to the Explicit Bundle ID created earlier

    e.g.

    ...
    id: 'my.company.testApp',
    ...
    
  • Azure utilizes yaml files to control it's build process, for now we are going to create a minimal file here as a place holder

    Create a new file build.yaml in the folder builds i.e. builds\build.yaml containing

    trigger:
    - main
    
    name: '1.1.$(DayOfYear)$(rev:rr)'
    #variables:
    #  - group: AzureDemoVariables 
    
    jobs:
    - job: BuildApple
      pool:
        vmImage: macOS-latest
      steps:
      - checkout: self
        clean: true 
        path: "TestApp"
    
      - script: |
          echo Saying Hello
        displayName: 'Saying Hello'
    

    Here we are instructing azure to run our Job on a mac, and have a simple script to say hello.

  • Check this project into GitHub

Build your app in Azure Devops

Setup an Azure Devops Free account

If you are not already a Microsoft Azure Devops user, you can setup a free account from Azure DevOps.

For this we have used GitHub credentials.

You will be asked to First Create an Organization, and this will then contain projects.

You will then be asked to create your first Project this will contain a reference to your GitHub project and will build your code.

Azure provides it's own git repositories, wiki, issue management etc. but for this example, we will only use the Pipeline feature to build our GitHub repository.

Enable building

Unfortunately you need to request the ability to build on azure for free accounts, you can read about it here, it boils down to having to fill out this form

Create new Pipeline

  • In the left menu click Pipelines and then Create Pipeline
  • Choose GitHub
  • Choose your Repo ( you may be prompted for GitHub credentials at this point depending on how you created your Azure Devops Account)
  • Approve and install the GitHub App for your repo
  • When asked to Configure your pipeline choose Existing Azure Pipelines YAML file
  • In the Path dropdown select the yaml file that was created earlier, /builds/build.yaml
  • Click Continue
  • Click The Down Arrow beside Run and Click Save
  • Your pipeline should now be saved and you can click Run Pipeline
  • This will fail with an error No hosted parallelism has been purchased or granted. Until your request above has been approved.
  • Once your request has been approved the pipeline should run successfully.

Setup required Nativescript dependencies

  • Add the following lines to builds\build.yaml

    
      - task: NodeTool@0 
        inputs:
          versionSpec: '14.x'
      - script: |
          python -m pip install six
        displayName: 'Install six'
    
      - script: |
          npm i -g nativescript
        displayName: 'Install nativescript'   
    
      - script: |
          npm --version
          node --version
    
          ruby --version
          gem query --local
          echo **************************************
          pip --version
          pip show six
    
        displayName: 'Show NPM version'
      - script: |
          ns --version --json
        displayName: 'Show nativescript version'
      - script: |
          ns doctor
        displayName: 'Run ns doctor'
    

    Here we are installing the NativeScript dependencies that are not already installed on the Image supplied by Microsoft.

    We lastly run ns doctor to check that all is OK.

  • Check it in and the job should trigger and run ( if not you can run manually)

Add Secrets to Azure project

We have 3 secrets that must be available on the mac in order to build our App.

  • development certificate
  • development profile
  • The password for the certificate

We store these in the Pipeline Library

  • In your azure project, navigate to Pipelines -> Library
  • Click on Secure Files
  • Click on +Secure file
  • Browse to the development.p12 created in previous steps and Click OK to upload.
  • Repeat this and upload the Profile ( .mobilprofile ) created previously.

Now we add a variable group which is used to hold the password and pointers to these files.

  • In your azure project, navigate to Pipelines -> Library
  • Choose the Variable groups tab and click + Variable Group
  • Give it he name AzureDemoVariables
  • Add a new variable name: P12password and set the value to be the password for the generated development.p12
  • Click the padlock at then end of the text box to change the variable type to secret, this means that you cannot read the value of the property.

Now we will add two variables which hold the id of the secret files we added previously.

  • Add a new variable name: apple_cert_sercure_file_id, we will set the value a little later

  • Add a new variable name: apple_provisioning_profile_file_id, we will set the value a little later

  • In your azure project, navigate to Pipelines -> Library

  • Click on Secure Files

  • Click on the development.p12 link

  • In the resulting url in the browser url field, there is a property secureField which e.g. secureFileId=abce9b6a-2470-4258-b6dc-48c26e026c3b this is the Id for the secret file, in this case abce9b6a-2470-4258-b6dc-48c26e026c3b.

  • Copy this Id to the value field of the variable we created apple_cert_sercure_file_id

  • Repeat this for the profile setting the value of the apple_provisioning_profile_file_id variable to it's secureFileId

  • It seems crazy that this is the way to get these ids but it is the only way I have found to do it

Now we have our variable group complete we can install them to the build machine.

Install Cert and Profile on build machine

  • Uncomment the variables section, i.e. it should read:

    variables:
      - group: AzureDemoVariables 
    
  • Add the following lines to the end of builds\build.yaml

      - task: InstallAppleCertificate@2
        displayName: 'Install an Apple certificate'
        inputs:
          certSecureFile: '$(apple_cert_sercure_file_id)'
          certPwd: '$(P12password)'
    
      - task: InstallAppleProvisioningProfile@1
        displayName: 'Install an Apple provisioning profile'
        inputs:
          provProfileSecureFile: '$(apple_provisioning_profile_file_id)'
    

This installs the cert and profile to the build machine, removing them when the job is complete.

  • Check it in and the job should trigger and run ( if not you can run manually)
  • Exampine the Pipeline Job, if it is asking for approval with the message This pipeline needs permission to access 3 resources before this run can continue , click View and grant the approvals.

Building the IPA

  • Add the following lines to the end of builds\build.yaml
      - script: |
          ns build ios --bundle --for-device
        displayName: 'Run ns to build'
    
      - task: CopyFiles@2
        displayName: 'Copy Files to: $(build.artifactstagingdirectory)'
        inputs:
          SourceFolder: '$(system.defaultworkingdirectory)'
          Contents: |
            **/*.ipa
          TargetFolder: '$(build.artifactstagingdirectory)'
          flattenFolders: true
          preserveTimestamp: true
          
      - task: PublishBuildArtifacts@1
        displayName: 'Publish Artifact: drop'
        inputs:
          PathtoPublish: '$(build.artifactstagingdirectory)'
    

Here we are building the app, then publishing the app to the pipelines artifacts, this makes the ipa available for download.

  • On the pipeline run screen, there will now be a link that should say 1 published clicking this will bring you to the artifacts page for that build run.
  • You can then download the ipa TestApp.ipas

Install the ipa on the iPhone

We are going to use OTA ( over the wire ) distribution to get the app onto the iPhone, this is where we host the app on our own site and download it to the phone from that site.

There are a couple of requirements for this:

  • The site must be served over https
  • The phone must trust the CA that issued the SSL cert which the site is using

If you are in a position to use certs from an authority ( like let's encrypt ) then you can use those certs and serve the app from a site using those certs, but here we will do it from scratch with our own certificates.

Create the certificates

We are going to use openssl again to create a root CA certificate, and then use this to generate a certificate that our site that will serve the ipa to our phone.

This requires that our phone can talk to our pc, and that a port ( 8888 ) is open on on our pc's firewall.

  • First we need our pc's ipaddress, issue the command ipconfig, this will output the various connections, you will want whichever is addresable on your LAN. ( usually 192.168.X.X ).

  • Create a folder in the project serve/certs

  • Create a file 'thesite.ext' in the folder with the contents:

    authorityKeyIdentifier=keyid,issuer
    basicConstraints=CA:FALSE
    keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
    subjectAltName = @alt_names
    
    [alt_names]
    DNS.1 = thesite.com
    IP.1 = 192.168.178.22
    
  • Replace the values for DNS.1 with your machine fqdn and IP.1 with your machines ip address

  • Create a batch file createcerts.bat in the serve/certs folder with the following contents:

    rem create the root CA Key
    openssl genrsa -des3 -out myCA.key  -passout pass:capassword 2048
    
    rem create the root CA cert
    openssl req -x509 -new -nodes -key myCA.key -sha256 -days 1825 -out myCA.pem -passin pass:capassword  -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=CA.example.com"
    
    rem create private key for site
    openssl genrsa -out thesite.key -passout pass:sitepassword 2048
    
    rem create CSR for site
    openssl req -new -key thesite.key -out thesite.csr -passin pass:sitepassword -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=site.example.com"
    
    rem create cert for site that serves the ipa
    openssl x509 -req -in thesite.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial -out thesite.crt -days 825 -sha256 -extfile thesite.ext -passin pass:capassword 
    

    Here we are creating a root CA key and certificate, then a web site key and certificate, you can edit the passwords/details but these will work for demo purposes.

  • Run the batch file in the serve/certs directory.

  • Now you will have the certificates required to serve the app from a site on your PC to your phone.

  • Email the myCA.pem to an account you can access on your iPhone.

  • From the email on your phone, click on the certificate and install the profile.

  • On the phone goto Settings and there should be a new option near the top Profile Downloaded click into this.

  • Click Install and enter your passcode.

  • Confirm by clicking install.

  • Navigate to Settings -> General -> About -> Certificate Trust Settings

  • You should see in the section ENABLE FULL TRUST FOR ROOT CERTIFICATE a listing for CA.example.com, toggle the switch to on and click continue.

Your phone will now trust certificates issues from your new self generated root CA.

For this reason you should keen these certificates safe, not check them into public repos and use your own passwords in the commands above.

Serve the IPA from your PC to your Phone

  • In the folder serve create a file download.plist This is the file that will be initially sent to your phone telling the phone where to get your app.
  • Add the contents:
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>items</key>
        <array>
            <dict>
                <key>assets</key>
                <array>
                    <dict>
                        <key>kind</key>
                        <string>software-package</string>
                        <key>url</key>
                        <string>https://mysite</string>
                    </dict>
                </array>
                <key>metadata</key>
                <dict>
                    <key>bundle-identifier</key>
                    <string>my.app.id</string>
                    <key>bundle-version</key>
                    <string>1.0.0</string>
                    <key>kind</key>
                    <string>software</string>
                    <key>title</key>
                    <string>MyApp</string>
                </dict>
            </dict>
        </array>
    </dict>
    </plist>
    

Now we will create node https server to serve the ipa

  • Create a file serve\serve.js

  • Add the contents:

    var http = require('https');
    var fs = require('fs');
    const myArgs = process.argv.slice(2);
    var ipaddress = myArgs[0];
    
    const port = 8888;
    
    const options = {
        key: fs.readFileSync('./serve/certs/thesite.key', 'utf8'),
        cert: fs.readFileSync('./serve/certs/thesite.crt', 'utf8'),
        ca: fs.readFileSync('./serve/certs/myCA.pem')
      };
    
    console.log(options.key);
    
    http.createServer(options, function (request, response) {
        console.log('request starting...');
        var filePath;
        var contentType;
        console.log(request.url);
    
        if(request.url === '/ipa'){
            filePath = './serve/ipa/TestApp.ipa';
            contentType = 'application/octet-stream';
        }else if (request.url === '/plist') {
            contentType = 'application/x-plist';
            filePath = './serve/download.plist';
        }
        if(filePath){
            fs.readFile(filePath, function(error, content) {
                if (error) {
    
                        response.writeHead(500);
                        response.end('An Error Occured: '+error.code+' ..\n');
                        response.end(); 
                }
                else {
                    var contentToWrite;
                    if(contentType == 'application/x-plist'){
                        contentToWrite=content.toString().replace("mysite", ipaddress+':'+port+'/ipa');
                    } else {
                        contentToWrite = content;
                    }
                    response.writeHead(200, { 'Content-Type': contentType });
                    response.end(contentToWrite);
                }
            }
        ) 
    
        } else {
            response.writeHead(200, { 'Content-Type': 'text/html' });
            response.end("hello Click the link to download an app<br><a  href='itms-services://?action=download-manifest&url=https://"+ ipaddress +":"+port+"/plist'>Download    </a>");
        }
      
    }).listen(port);
    

    This will create https server which will use our generated certificates and serve our IPA to the phone.

  • Take your downloaded IPA and place it at serve/ipa/TestApp.ipa

  • Run the https server from the root of your project

    node serve\serve.js <Your Ip Address>
    
  • From your phone, browse to https://<your ip address>:8888

  • Click the download link

  • You will be prompted to install your app

The App running on your IPhone

You can get the log output from the IPhone while it is connected to your PC.

  • Issue the command used above again, while your phone is connected.

    ns device
    
  • If this shows the device is connected, You can run

    ns device log > LogOutput.txt
    
  • This will log out a lot of information, most of it you will not want, but you will see CONSOLE LOG messages in there from your app, as well as other errors etc.

    Example output:

    Jan 18 17:52:14 jason-iphone SpringBoard(RunningBoardServices)[50750] : Received state update for 53575
    Jan 18 17:52:14 jason-iphone SpringBoard(RunningBoardServices)[50750] : Received state update for 53533
    Jan 18 17:52:14 jason-iphone TestApp(NativeScript)[53575] : CONSOLE LOG: Angular is running in development mode. Call enableProdMode() to enable production mode.
    Jan 18 17:52:14 jason-iphone kernel[0] : 1609773.409 memorystatus: killing_idle_process pid 53361 [com.apple.WebKit.WebContent] (sustained-memory-pressure 0) 5216KB - memorystatus_available_pages: 66771 compressor_size:120141
    Jan 18 17:52:14 jason-iphone UserEventAgent(MemoryMonitor)[50714] : kernel jetsam snapshot note received
    

Final Notes

In Yaml whitespace matters, a sample project is available here which contains a project utilizing the above.

It should be obvious if you have made it this far that the above is not really suitable if you are constantly making changes to your app, the turn around time is just too long, but cloud builds can at least let you dip your toe into the water of iOS development without an outlay for a Mac.