Developing a NativeScript app for iOS when you don't have a Mac
Even if you don't have a Mac, you can develop for iOS with the help of Cloud builds on Azure.
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
- Create our GitHub project
- Build your app in Azure Devops
- Install the ipa on the iPhone
- Final Notes
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 PEMGenerate 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
Devicesand 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 deviceThe
Device Identifierlisted for your phone is the Device ID (UDID), which you should enter. -
Click
Continuethrough the remaining screens.
Create an App Identifier
- Login to your apple developer account
- Choose
Certificates, IDs & Profiles - Choose
Identifiersand click the+icon - Select
App IDsand clickContinue - Choose Type
Appand clickContinue - Enter a
Descriptionand anExplicit 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
Profilesand click the+icon - Choose
iOS App Developmentand clickContinue - Select the
App IDthat you created in the previous step (e.g. my.company.testApp) and clickContinue - 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.tschanging the id to theExplicit Bundle IDcreated earliere.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.yamlin the folder builds i.e.builds\build.yamlcontainingtrigger: - 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
Pipelinesand thenCreate 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 Appfor your repo - When asked to
Configure your pipelinechooseExisting Azure Pipelines YAML file - In the
Pathdropdown select the yaml file that was created earlier,/builds/build.yaml - Click
Continue - Click The Down Arrow beside
Runand ClickSave - 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 doctorto 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.p12created in previous steps and ClickOKto 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 groupstab and click+ Variable Group - Give it he name
AzureDemoVariables - Add a new variable name:
P12passwordand set the value to be the password for the generateddevelopment.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-48c26e026c3bthis is the Id for the secret file, in this caseabce9b6a-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_idvariable to it'ssecureFileId -
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, clickViewand 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 publishedclicking 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.batin theserve/certsfolder 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:capasswordHere 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/certsdirectory. -
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
Settingsand there should be a new option near the topProfile Downloadedclick 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 CERTIFICATEa listing forCA.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
servecreate a filedownload.plistThis 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 LOGmessages 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.