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.
You will need to enroll in the Apple Developer Program.
Once you have enrolled the next step is to create a development certificate.
+
to add a new certificate.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.
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.
Certificates, IDs & Profiles
Identifiers
and click the +
iconApp IDs
and click Continue
App
and click Continue
Description
and an Explicit Bundle ID
, e.g. my.company.testAppContinue
Register
Certificates, IDs & Profiles
Profiles
and click the +
iconiOS App Development
and click Continue
App ID
that you created in the previous step (e.g. my.company.testApp) and click Continue
Continue
Generate
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
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.
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
Pipelines
and then Create Pipeline
GitHub
GitHub App
for your repoConfigure your pipeline
choose Existing Azure Pipelines YAML file
Path
dropdown select the yaml file that was created earlier, /builds/build.yaml
Continue
Run
and Click Save
Run Pipeline
No hosted parallelism has been purchased or granted.
Until your request above has been approved.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)
We have 3 secrets that must be available on the mac in order to build our App.
We store these in the Pipeline
Library
Pipelines
-> Library
Secure Files
+Secure file
development.p12
created in previous steps and Click OK
to upload.Profile
( .mobilprofile
) created previously.Now we add a variable group which is used to hold the password and pointers to these files.
Pipelines
-> Library
Variable groups
tab and click + Variable Group
AzureDemoVariables
P12password
and set the value to be the password for the generated development.p12
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.
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.
This pipeline needs permission to access 3 resources before this run can continue
, click View
and grant the approvals.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.
1 published
clicking this will bring you to the artifacts page for that build run.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:
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.
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
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.<?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
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
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.