├── .github
├── FUNDING.yml
└── workflows
│ ├── cla.yaml
│ ├── publish.yml
│ └── test.yaml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── index.js
├── lib
├── certs.js
├── device.js
├── env.js
├── provisioning.js
├── sim_focus.scpt
├── sim_hide.scpt
├── simctl.js
├── simulator.js
├── teams.js
├── utilities.js
└── xcode.js
├── package-lock.json
├── package.json
└── test
├── TestApp
├── TestApp.xcodeproj
│ ├── project.pbxproj
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── TestApp.xcscheme
└── TestApp
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Base.lproj
│ ├── Main_iPad.storyboard
│ └── Main_iPhone.storyboard
│ ├── Images.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── LaunchImage.launchimage
│ │ └── Contents.json
│ ├── TestApp-Info.plist
│ ├── TestApp-Prefix.pch
│ ├── ViewController.h
│ ├── ViewController.m
│ ├── en.lproj
│ └── InfoPlist.strings
│ └── main.m
├── TestWatchApp
├── TestWatchApp WatchKit App
│ ├── Base.lproj
│ │ └── Interface.storyboard
│ ├── Images.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ └── Info.plist
├── TestWatchApp WatchKit Extension
│ ├── GlanceController.h
│ ├── GlanceController.m
│ ├── Images.xcassets
│ │ └── README__ignoredByTemplate__
│ ├── Info.plist
│ ├── InterfaceController.h
│ ├── InterfaceController.m
│ ├── NotificationController.h
│ ├── NotificationController.m
│ └── PushNotificationPayload.apns
├── TestWatchApp.xcodeproj
│ ├── project.pbxproj
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── Glance - TestWatchApp WatchKit App.xcscheme
│ │ ├── Notification - TestWatchApp WatchKit App.xcscheme
│ │ ├── TestWatchApp WatchKit App.xcscheme
│ │ └── TestWatchApp.xcscheme
└── TestWatchApp
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Base.lproj
│ ├── Main_iPad.storyboard
│ └── Main_iPhone.storyboard
│ ├── Images.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── LaunchImage.launchimage
│ │ └── Contents.json
│ ├── TestWatchApp-Info.plist
│ ├── TestWatchApp-Prefix.pch
│ ├── ViewController.h
│ ├── ViewController.m
│ ├── en.lproj
│ └── InfoPlist.strings
│ └── main.m
├── TestWatchApp2
├── TestWatchApp2 WatchKit App
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Base.lproj
│ │ └── Interface.storyboard
│ └── Info.plist
├── TestWatchApp2 WatchKit Extension
│ ├── Assets.xcassets
│ │ └── README__ignoredByTemplate__
│ ├── ExtensionDelegate.h
│ ├── ExtensionDelegate.m
│ ├── Info.plist
│ ├── InterfaceController.h
│ ├── InterfaceController.m
│ ├── NotificationController.h
│ ├── NotificationController.m
│ └── PushNotificationPayload.apns
├── TestWatchApp2.xcodeproj
│ ├── project.pbxproj
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── Notification - TestWatchApp2 WatchKit App.xcscheme
│ │ ├── TestWatchApp2 WatchKit App.xcscheme
│ │ └── TestWatchApp2.xcscheme
└── TestWatchApp2
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Info.plist
│ ├── ViewController.h
│ ├── ViewController.m
│ └── main.m
├── init.js
├── test-certs.js
├── test-device.js
├── test-env.js
├── test-ioslib.js
├── test-provisioning.js
├── test-simulator.js
├── test-teams.js
└── test-xcode.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: tidev
2 | liberapay: tidev
3 |
--------------------------------------------------------------------------------
/.github/workflows/cla.yaml:
--------------------------------------------------------------------------------
1 | name: Check CLA
2 | on:
3 | - pull_request
4 |
5 | jobs:
6 | check-cla:
7 | runs-on: ubuntu-latest
8 | name: Verify contributor
9 |
10 | steps:
11 | - uses: tidev/tidev-cla-action@v2
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
14 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | release:
4 | types: [ published ]
5 |
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | name: Publish
10 |
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 |
15 | - name: Setup node
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: 20
19 | registry-url: 'https://registry.npmjs.org'
20 |
21 | - name: Install dependencies
22 | run: npm ci
23 | if: steps.node-cache.outputs.cache-hit != 'true'
24 |
25 | - name: Publish to npm
26 | env:
27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28 | run: npm publish --tag ${{ github.event.release.prerelease && 'next' || 'latest' }}
29 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | - pull_request
4 | - push
5 |
6 | jobs:
7 | test:
8 | runs-on: macos-latest
9 | name: Test
10 |
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 |
15 | - name: Setup node
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: 20
19 | registry-url: 'https://registry.npmjs.org'
20 |
21 | - name: Install dependencies
22 | run: npm ci
23 | if: steps.node-cache.outputs.cache-hit != 'true'
24 |
25 | - name: Run tests
26 | run: npm test
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ._*
2 | .DS_Store
3 | dist
4 | npm-debug.log
5 | node_modules
6 | env.properties
7 | junit_report.xml
8 |
9 | test/TestApp/info.plist
10 | test/TestApp/build
11 | test/TestApp/index
12 | test/TestApp/Logs
13 | test/TestApp/ModuleCache
14 | test/TestApp/ModuleCache.noindex
15 | test/TestApp/TestApp.xcodeproj/xcuserdata
16 | test/TestApp/TestApp.xcodeproj/project.xcworkspace
17 |
18 | test/TestWatchApp/info.plist
19 | test/TestWatchApp/build
20 | test/TestWatchApp/index
21 | test/TestWatchApp/Logs
22 | test/TestWatchApp/ModuleCache
23 | test/TestWatchApp/ModuleCache.noindex
24 | test/TestWatchApp/TestWatchApp.xcodeproj/xcuserdata
25 | test/TestWatchApp/TestWatchApp.xcodeproj/project.xcworkspace
26 |
27 | test/TestWatchApp2/info.plist
28 | test/TestWatchApp2/build
29 | test/TestWatchApp2/index
30 | test/TestWatchApp2/Logs
31 | test/TestWatchApp2/ModuleCache
32 | test/TestWatchApp2/ModuleCache.noindex
33 | test/TestWatchApp2/TestWatchApp2.xcodeproj/xcuserdata
34 | test/TestWatchApp2/TestWatchApp2.xcodeproj/project.xcworkspace
35 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | ._*
2 | .DS_Store
3 | .git*
4 | /dist
5 | /node_modules
6 | /npm-debug.log
7 | /env.properties
8 |
9 | test/TestApp/info.plist
10 | test/TestApp/build
11 | test/TestApp/Build
12 | test/TestApp/Logs
13 | test/TestApp/ModuleCache
14 | test/TestApp/TestApp.xcodeproj/xcuserdata
15 | test/TestApp/TestApp.xcodeproj/project.xcworkspace
16 |
17 | test/TestWatchApp/info.plist
18 | test/TestWatchApp/build
19 | test/TestWatchApp/Build
20 | test/TestWatchApp/Logs
21 | test/TestWatchApp/ModuleCache
22 | test/TestWatchApp/TestWatchApp.xcodeproj/xcuserdata
23 | test/TestWatchApp/TestWatchApp.xcodeproj/project.xcworkspace
24 |
25 | test/TestWatchApp2/info.plist
26 | test/TestWatchApp2/build
27 | test/TestWatchApp2/Build
28 | test/TestWatchApp2/Logs
29 | test/TestWatchApp2/ModuleCache
30 | test/TestWatchApp2/TestWatchApp2.xcodeproj/xcuserdata
31 | test/TestWatchApp2/TestWatchApp2.xcodeproj/project.xcworkspace
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright TiDev, Inc. 4/7/2022-Present
2 | Copyright 2014-2020 by Appcelerator, Inc.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 |
16 | -------------------------------------------------------------------------------
17 |
18 | lib/certs.js contains code from the "forge" project.
19 | https://github.com/digitalbazaar/forge
20 |
21 | New BSD License (3-clause)
22 | Copyright (c) 2010, Digital Bazaar, Inc.
23 | All rights reserved.
24 |
25 | Redistribution and use in source and binary forms, with or without
26 | modification, are permitted provided that the following conditions are met:
27 | * Redistributions of source code must retain the above copyright
28 | notice, this list of conditions and the following disclaimer.
29 | * Redistributions in binary form must reproduce the above copyright
30 | notice, this list of conditions and the following disclaimer in the
31 | documentation and/or other materials provided with the distribution.
32 | * Neither the name of Digital Bazaar, Inc. nor the
33 | names of its contributors may be used to endorse or promote products
34 | derived from this software without specific prior written permission.
35 |
36 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
37 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
38 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
39 | DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY
40 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
41 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
42 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
45 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # iOS Utility Library
2 |
3 | > This is a library of utilities for dealing programmatically with iOS applications,
4 | used namely for tools like [Hyperloop](https://github.com/tidev/hyperloop)
5 | and [Titanium SDK](https://github.com/tidev/titanium-sdk).
6 |
7 | ioslib supports Xcode 6 and newer.
8 |
9 | ## Installation
10 |
11 | From NPM:
12 |
13 | npm install ioslib
14 |
15 | ## Examples
16 |
17 | ### Detect all the connected iOS devices:
18 |
19 | ```javascript
20 | var ioslib = require('ioslib');
21 |
22 | ioslib.device.detect(function (err, devices) {
23 | if (err) {
24 | console.error(err);
25 | } else {
26 | console.log(devices);
27 | }
28 | });
29 | ```
30 |
31 | ### Install an application on device
32 |
33 | ```javascript
34 | var deviceUDID = null; // string or null to pick first device
35 |
36 | ioslib.device.install(deviceUDID, '/path/to/name.app', 'com.company.appname')
37 | .on('installed', function () {
38 | console.log('App successfully installed on device');
39 | })
40 | .on('appStarted', function () {
41 | console.log('App has started');
42 | })
43 | .on('log', function (msg) {
44 | console.log('[LOG] ' + msg);
45 | })
46 | .on('appQuit', function () {
47 | console.log('App has quit');
48 | })
49 | .on('error', function (err) {
50 | console.error(err);
51 | });
52 | ```
53 |
54 | ### Launch the iOS Simulator
55 |
56 | ```javascript
57 | ioslib.simulator.launch(null, function (err, simHandle) {
58 | console.log('Simulator launched');
59 | ioslib.simulator.stop(simHandle, function () {
60 | console.log('Simulator stopped');
61 | });
62 | });
63 | ```
64 |
65 | ### Launch, install, and run an application on simulator
66 |
67 | ```javascript
68 | var simUDID = null; // string or null to pick a simulator
69 |
70 | ioslib.simulator.launch(simUDID, {
71 | appPath: '/path/to/name.app'
72 | })
73 | .on('launched', function (msg) {
74 | console.log('Simulator has launched');
75 | })
76 | .on('appStarted', function (msg) {
77 | console.log('App has started');
78 | })
79 | .on('log', function (msg) {
80 | console.log('[LOG] ' + msg);
81 | })
82 | .on('error', function (err) {
83 | console.error(err);
84 | });
85 | ```
86 |
87 | ### Force stop an application running on simulator
88 |
89 | ```javascript
90 | ioslib.simulator.launch(simUDID, {
91 | appPath: '/path/to/name.app'
92 | })
93 | .on('launched', function (simHandle) {
94 | console.log('Simulator launched');
95 | ioslib.simulator.stop(simHandle).on('stopped', function () {
96 | console.log('Simulator stopped');
97 | });
98 | });
99 | ```
100 |
101 | ### Find a valid device/cert/provisioning profile combination
102 |
103 | ```javascript
104 | ioslib.findValidDeviceCertProfileCombos({
105 | appId: 'com.company.appname'
106 | }, function (err, results) {
107 | if (err) {
108 | console.error(err);
109 | } else {
110 | console.log(results);
111 | }
112 | });
113 | ```
114 |
115 | ### Detect everything
116 |
117 | ```javascript
118 | ioslib.detect(function (err, info) {
119 | if (err) {
120 | console.error(err);
121 | } else {
122 | console.log(info);
123 | }
124 | });
125 | ```
126 |
127 | ### Detect iOS certificates
128 |
129 | ```javascript
130 | ioslib.certs.detect(function (err, certs) {
131 | if (err) {
132 | console.error(err);
133 | } else {
134 | console.log(certs);
135 | }
136 | });
137 | ```
138 |
139 | ### Detect provisioning profiles
140 |
141 | ```javascript
142 | ioslib.provisioning.detect(function (err, profiles) {
143 | if (err) {
144 | console.error(err);
145 | } else {
146 | console.log(profiles);
147 | }
148 | });
149 | ```
150 |
151 | ### Detect Xcode installations
152 |
153 | ```javascript
154 | ioslib.xcode.detect(function (err, xcodeInfo) {
155 | if (err) {
156 | console.error(err);
157 | } else {
158 | console.log(xcodeInfo);
159 | }
160 | });
161 | ```
162 |
163 | ## Running Tests
164 |
165 | For best results, connect an iOS device.
166 |
167 | To run all tests:
168 |
169 | ```
170 | npm test
171 | ```
172 |
173 | To see debug logging, set the `DEBUG` environment variable:
174 |
175 | ```
176 | DEBUG=1 npm test
177 | ```
178 |
179 | To run a specific test suite:
180 |
181 | ```
182 | npm run-script test-certs
183 |
184 | npm run-script test-device
185 |
186 | npm run-script test-env
187 |
188 | npm run-script test-ioslib
189 |
190 | npm run-script test-provisioning
191 |
192 | npm run-script test-simulator
193 |
194 | npm run-script test-xcode
195 | ```
196 |
197 | ## Contributing
198 |
199 | Interested in contributing? There are several ways you can help contribute to this project.
200 |
201 | ### New Features, Improvements, Bug Fixes, & Documentation
202 |
203 | Source code contributions are always welcome! Before we can accept your pull request, you must sign a Contributor License Agreement (CLA). Please visit https://tidev.io/contribute for more information.
204 |
205 | ### Donations
206 |
207 | Please consider supporting this project by making a charitable [donation](https://tidev.io/donate). The money you donate goes to compensate the skilled engineeers and maintainers that keep this project going.
208 |
209 | ### Code of Conduct
210 |
211 | TiDev wants to provide a safe and welcoming community for everyone to participate. Please see our [Code of Conduct](https://tidev.io/code-of-conduct) that applies to all contributors.
212 |
213 | ## Security
214 |
215 | If you find a security related issue, please send an email to [security@tidev.io](mailto:security@tidev.io) instead of publicly creating a ticket.
216 |
217 | ## Stay Connected
218 |
219 | For the latest information, please find us on Twitter: [Titanium SDK](https://twitter.com/titaniumsdk) and [TiDev](https://twitter.com/tidevio).
220 |
221 | Join our growing Slack community by visiting https://slack.tidev.io!
222 |
223 | ## Legal
224 |
225 | Titanium is a registered trademark of TiDev Inc. All Titanium trademark and patent rights were transferred and assigned to TiDev Inc. on 4/7/2022. Please see the LEGAL information about using our trademarks, privacy policy, terms of usage and other legal information at https://tidev.io/legal.
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Main namespace for the ioslib.
3 | *
4 | * @copyright
5 | * Copyright (c) 2014-2016 by Appcelerator, Inc. All Rights Reserved.
6 | *
7 | * @license
8 | * Licensed under the terms of the Apache Public License.
9 | * Please see the LICENSE included with this distribution for details.
10 | */
11 |
12 | const
13 | async = require('async'),
14 |
15 | certs = exports.certs = require('./lib/certs'),
16 | device = exports.device = require('./lib/device'),
17 | env = exports.env = require('./lib/env'),
18 | magik = exports.magik = require('./lib/utilities').magik,
19 | provisioning = exports.provisioning = require('./lib/provisioning'),
20 | simulator = exports.simulator = require('./lib/simulator'),
21 | teams = exports.teams = require('./lib/teams'),
22 | utilities = exports.utilities = require('./lib/utilities'),
23 | xcode = exports.xcode = require('./lib/xcode');
24 |
25 | var cache;
26 |
27 | exports.detect = detect;
28 | exports.findValidDeviceCertProfileCombos = findValidDeviceCertProfileCombos;
29 |
30 | /**
31 | * Detects the entire iOS environment information.
32 | *
33 | * @param {Object} [options] - An object containing various settings.
34 | * @param {Boolean} [options.bypassCache=false] - When true, re-detects the all iOS information.
35 | * @param {String} [options.minIosVersion] - The minimum iOS SDK to detect.
36 | * @param {String} [options.minWatchosVersion] - The minimum WatchOS SDK to detect.
37 | * @param {String} [options.profileDir=~/Library/Developer/Xcode/UserData/Provisioning Profiles] - The path to search for provisioning profiles.
38 | * @param {String} [options.security] - Path to the security
executable
39 | * @param {String} [options.supportedVersions] - A string with a version number or range to check if an Xcode install is supported.
40 | * @param {String} [options.type] - The type of emulators to return. Can be either "iphone" or "ipad". Defaults to all types.
41 | * @param {Boolean} [options.validOnly=true] - When true, only returns non-expired, valid certificates.
42 | * @param {String} [options.xcodeSelect] - Path to the xcode-select
executable
43 | * @param {Function} [callback(err, info)] - A function to call when all detection tasks have completed.
44 | */
45 | function detect(options, callback) {
46 | return magik(options, callback, function (emitter, options, callback) {
47 | if (cache && !options.bypassCache) {
48 | emitter.emit('detected', cache);
49 | return callback(null, cache);
50 | }
51 |
52 | var results = {
53 | detectVersion: '5.0',
54 | issues: []
55 | };
56 |
57 | function mix(src, dest) {
58 | Object.keys(src).forEach(function (name) {
59 | if (Array.isArray(src[name])) {
60 | if (Array.isArray(dest[name])) {
61 | dest[name] = dest[name].concat(src[name]);
62 | } else {
63 | dest[name] = src[name];
64 | }
65 | } else if (src[name] !== null && typeof src[name] === 'object') {
66 | dest[name] || (dest[name] = {});
67 | Object.keys(src[name]).forEach(function (key) {
68 | dest[name][key] = src[name][key];
69 | });
70 | } else {
71 | dest[name] = src[name];
72 | }
73 | });
74 | }
75 |
76 | async.parallel([
77 | function detectCertificates(done) {
78 | certs.detect(options, function (err, result) {
79 | err || mix(result, results);
80 | done(err);
81 | });
82 | },
83 | function detectDevices(done) {
84 | device.detect(options, function (err, result) {
85 | err || mix(result, results);
86 | done(err);
87 | });
88 | },
89 | function detectEnvironment(done) {
90 | env.detect(options, function (err, result) {
91 | err || mix(result, results);
92 | done(err);
93 | });
94 | },
95 | function detectProvisioning(done) {
96 | provisioning.detect(options, function (err, result) {
97 | err || mix(result, results);
98 | done(err);
99 | });
100 | },
101 | function detectSimulator(done) {
102 | simulator.detect(options, function (err, result) {
103 | err || mix(result, results);
104 | done(err);
105 | });
106 | },
107 | function detectTeams(done) {
108 | teams.detect(options, function (err, result) {
109 | err || mix(result, results);
110 | done(err);
111 | });
112 | },
113 | function detectXcode(done) {
114 | xcode.detect(options, function (err, result) {
115 | err || mix(result, results);
116 | done(err);
117 | });
118 | }
119 | ], function (err) {
120 | if (err) {
121 | emitter.emit('error', err);
122 | return callback(err);
123 | } else {
124 | cache = results;
125 | emitter.emit('detected', results);
126 | return callback(null, results);
127 | }
128 | });
129 | });
130 | };
131 |
132 | /**
133 | * Finds all valid device/cert/provisioning profile combinations. This is handy for quickly
134 | * finding valid parameters for building an app for an iOS device.
135 | *
136 | * @param {Object} [options] - An object containing various settings.
137 | * @param {String} [options.appId] - The app identifier (com.domain.app) to filter provisioning profiles by.
138 | * @param {Boolean} [options.bypassCache=false] - When true, re-detects the all iOS information.
139 | * @param {Boolean} [options.unmanagedProvisioningProfile] - When true, selects an unmanaged provisioning profile.
140 | * @param {Function} [callback(err, info)] - A function to call when the simulator has launched.
141 | */
142 | function findValidDeviceCertProfileCombos(options, callback) {
143 | if (typeof options === 'function') {
144 | callback = options;
145 | options = {};
146 | } else if (!options) {
147 | options = {};
148 | }
149 | typeof callback === 'function' || (callback = function () {});
150 |
151 | // find us a device
152 | device.detect(function (err, deviceResults) {
153 | if (!deviceResults.devices.length) {
154 | // no devices connected
155 | return callback(new Error('No iOS devices connected'));
156 | }
157 |
158 | // next find us some certs
159 | certs.detect(function (err, certResults) {
160 | var certs = [];
161 | Object.keys(certResults.certs.keychains).forEach(function (keychain) {
162 | var types = certResults.certs.keychains[keychain];
163 | Object.keys(types).forEach(function (type) {
164 | certs = certs.concat(types[type]);
165 | });
166 | });
167 |
168 | if (!certs.length) {
169 | return callback(new Error('No iOS certificates'));
170 | }
171 |
172 | // find us a provisioning profile
173 | provisioning.find({
174 | appId: options.appId,
175 | certs: certs,
176 | devicesUDIDs: deviceResults.devices.map(function (device) { return device.udid; }),
177 | unmanaged: options.unmanagedProvisioningProfile
178 | }, function (err, profiles) {
179 | if (!profiles.length) {
180 | return callback(new Error('No provisioning profiles found'));
181 |
182 | }
183 |
184 | var combos = [];
185 | profiles.forEach(function (profile) {
186 | deviceResults.devices.forEach(function (device) {
187 | if (profile.devices && profile.devices.indexOf(device.udid) !== -1) {
188 | certs.forEach(function (cert) {
189 | var prefix = cert.pem.replace(/^-----BEGIN CERTIFICATE-----\n/, '').substring(0, 60);
190 | profile.certs.forEach(function (pcert) {
191 | if (pcert.indexOf(prefix) === 0) {
192 | combos.push({
193 | ppUUID: profile.uuid,
194 | certName: cert.name,
195 | deviceUDID: device.udid
196 | });
197 | }
198 | });
199 | });
200 | }
201 | });
202 | });
203 |
204 | callback(null, combos);
205 | });
206 | });
207 | });
208 | }
209 |
--------------------------------------------------------------------------------
/lib/device.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Detects iOS developer and distribution certificates and the WWDC certificate.
3 | *
4 | * @module device
5 | *
6 | * @copyright
7 | * Copyright (c) 2014-2016 by Appcelerator, Inc. All Rights Reserved.
8 | *
9 | * @license
10 | * Licensed under the terms of the Apache Public License.
11 | * Please see the LICENSE included with this distribution for details.
12 | */
13 |
14 | 'use strict';
15 |
16 | const appc = require('node-appc');
17 | const async = require('async');
18 | const magik = require('./utilities').magik;
19 | const fs = require('fs');
20 | const iosDevice = require('node-ios-device');
21 | const path = require('path');
22 | const __ = appc.i18n(__dirname).__;
23 |
24 | var cache;
25 |
26 | exports.detect = detect;
27 | exports.install = install;
28 |
29 | /**
30 | * Detects connected iOS devices.
31 | *
32 | * @param {Object} [options] - An object containing various settings.
33 | * @param {Boolean} [options.bypassCache=false] - When true, re-detects all connected iOS devices.
34 | * @param {Function} [callback(err, results)] - A function to call with the device information.
35 | *
36 | * @emits module:device#detected
37 | * @emits module:device#error
38 | *
39 | * @returns {Handle}
40 | */
41 | function detect(options, callback) {
42 | return magik(options, callback, function (handle, options, callback) {
43 | if (cache && !options.bypassCache) {
44 | var dupe = JSON.parse(JSON.stringify(cache));
45 | handle.emit('detected', dupe);
46 | return callback(null, dupe);
47 | }
48 |
49 | iosDevice.devices(function (err, devices) {
50 | if (err) {
51 | handle.emit('error', err);
52 | return callback(err);
53 | }
54 |
55 | var results = {
56 | devices: devices,
57 | issues: []
58 | };
59 |
60 | // the cache must be a clean copy that we'll clone for subsequent detect() calls
61 | // because we can't allow the cache to be modified by reference
62 | cache = JSON.parse(JSON.stringify(results));
63 |
64 | handle.emit('detected', results);
65 | return callback(null, results);
66 | });
67 | });
68 | };
69 |
70 | /**
71 | * Installs the specified app to an iOS device.
72 | *
73 | * @param {String} udid - The UDID of the device to install the app to or null if you want ioslib to pick one.
74 | * @param {String} appPath - The path to the iOS app to install after launching the iOS Simulator.
75 | * @param {Object} [options] - An object containing various settings.
76 | * @param {Boolean} [options.bypassCache=false] - When true, re-detects all iOS simulators.
77 | * @param {Number} [options.logPort] - A port to connect to in the iOS app and relay log messages from.
78 | * @param {Number} [options.timeout] - Number of milliseconds to wait before timing out.
79 | *
80 | * @emits module:device#app-quit - Only omitted when `options.logPort` is specified and app starts a TCP server.
81 | * @emits module:device#app-started - Only omitted when `options.logPort` is specified and app starts a TCP server.
82 | * @emits module:device#disconnect - Only omitted when `options.logPort` is specified and app starts a TCP server.
83 | * @emits module:device#error
84 | * @emits module:device#installed
85 | * @emits module:device#log - Only omitted when `options.logPort` is specified and app starts a TCP server.
86 | *
87 | * @returns {Handle}
88 | */
89 | function install(udid, appPath, options) {
90 | return magik(options, null, function (handle, options) {
91 | if (!appPath) {
92 | return handle.emit('error', new Error(__('Missing app path argument')));
93 | }
94 |
95 | if (!fs.existsSync(appPath)) {
96 | return handle.emit('error', new Error(__('App path does not exist: ' + appPath)));
97 | }
98 |
99 | handle.stop = function () {}; // for stopping logging
100 |
101 | iosDevice.installApp(udid, appPath, function (err) {
102 | if (err) {
103 | return handle.emit('error', err);
104 | }
105 |
106 | handle.emit('installed');
107 |
108 | if (options.logPort) {
109 | var logHandle = iosDevice
110 | .log(udid, options.logPort)
111 | .on('log', function (msg) {
112 | handle.emit('log', msg);
113 | })
114 | .on('app-started', function () {
115 | handle.emit('app-started');
116 | })
117 | .on('app-quit', function () {
118 | handle.emit('app-quit');
119 | })
120 | .on('disconnect', function () {
121 | handle.emit('disconnect');
122 | })
123 | .on('error', function (err) {
124 | handle.emit('log-error', err);
125 | });
126 |
127 | handle.stop = function () {
128 | logHandle.stop();
129 | };
130 | }
131 | });
132 | });
133 | }
134 |
--------------------------------------------------------------------------------
/lib/env.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Detects the iOS development environment.
3 | *
4 | * @module env
5 | *
6 | * @copyright
7 | * Copyright (c) 2014-2017 by Appcelerator, Inc. All Rights Reserved.
8 | *
9 | * @license
10 | * Licensed under the terms of the Apache Public License.
11 | * Please see the LICENSE included with this distribution for details.
12 | */
13 |
14 | const
15 | appc = require('node-appc'),
16 | async = require('async'),
17 | magik = require('./utilities').magik,
18 | __ = appc.i18n(__dirname).__;
19 |
20 | var cache = null;
21 |
22 | /**
23 | * Fired when the developer profiles have been updated.
24 | * @event module:env#detected
25 | * @type {Object}
26 | */
27 |
28 | /**
29 | * Fired when there was an error retreiving the provisioning profiles.
30 | * @event module:env#error
31 | * @type {Error}
32 | */
33 |
34 | /**
35 | * Detects the iOS development enviroment dependencies.
36 | *
37 | * @param {Object} [options] - An object containing various settings
38 | * @param {Boolean} [options.bypassCache=false] - When true, re-detects the development environment dependencies
39 | * @param {String} [options.security] - Path to the security
executable
40 | * @param {String} [options.xcodeSelect] - Path to the xcode-select
executable
41 | * @param {Function} [callback(err, results)] - A function to call with the development environment information
42 | *
43 | * @emits module:env#detected
44 | * @emits module:env#error
45 | *
46 | * @returns {Handle}
47 | */
48 | exports.detect = function detect(options, callback) {
49 | return magik(options, callback, function (emitter, options, callback) {
50 | if (cache && !options.bypassCache) {
51 | return callback(null, cache);
52 | }
53 |
54 | var results = {
55 | executables: {
56 | xcodeSelect: null,
57 | security: null
58 | },
59 | issues: []
60 | };
61 |
62 | async.parallel({
63 | security: function (next) {
64 | appc.subprocess.findExecutable([options.security, '/usr/bin/security', 'security'], function (err, result) {
65 | if (err) {
66 | results.issues.push({
67 | id: 'IOS_SECURITY_EXECUTABLE_NOT_FOUND',
68 | type: 'error',
69 | message: __("Unable to find the 'security' executable.") + '\n'
70 | + __('Please verify your system path.') + '\n'
71 | + __("This program is distributed with macOS and if it's missing, you'll have to restore it from a backup or another computer, or reinstall macOS.")
72 | });
73 | } else {
74 | results.executables.security = result;
75 | }
76 | next();
77 | });
78 | },
79 |
80 | xcodeSelect: function (next) {
81 | appc.subprocess.findExecutable([options.xcodeSelect, '/usr/bin/xcode-select', 'xcode-select'], function (err, result) {
82 | if (err) {
83 | results.issues.push({
84 | id: 'IOS_XCODE_SELECT_EXECUTABLE_NOT_FOUND',
85 | type: 'error',
86 | message: __("Unable to find the 'xcode-select' executable.") + '\n'
87 | + __('Perhaps Xcode is not installed, your Xcode installation is corrupt, or your system path is incomplete.')
88 | });
89 | } else {
90 | results.executables.xcodeSelect = result;
91 | }
92 | next();
93 | });
94 | }
95 | }, function () {
96 | cache = results;
97 | emitter.emit('detected', results);
98 | callback(null, results);
99 | });
100 | });
101 | };
102 |
--------------------------------------------------------------------------------
/lib/provisioning.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Detects provisioning profiles.
3 | *
4 | * @module provisioning
5 | *
6 | * @copyright
7 | * Copyright (c) 2014-2016 by Appcelerator, Inc. All Rights Reserved.
8 | *
9 | * @license
10 | * Licensed under the terms of the Apache Public License.
11 | * Please see the LICENSE included with this distribution for details.
12 | *
13 | * @requires certs
14 | */
15 |
16 | const
17 | appc = require('node-appc'),
18 | certs = require('./certs'),
19 | magik = require('./utilities').magik,
20 | fs = require('fs'),
21 | path = require('path'),
22 | __ = appc.i18n(__dirname).__,
23 | provisioningProfilesDirectories = [
24 | '~/Library/Developer/Xcode/UserData/Provisioning Profiles',
25 | '~/Library/MobileDevice/Provisioning Profiles'
26 | ]
27 |
28 | var cache = null,
29 | watchers = {};
30 |
31 | /**
32 | * Fired when the provisioning profiles have been detected or updated.
33 | * @event module:provisioning#detected
34 | * @type {Object}
35 | */
36 |
37 | /**
38 | * Fired when there was an error retreiving the provisioning profiles.
39 | * @event module:provisioning#error
40 | * @type {Error}
41 | */
42 |
43 | exports.detect = detect;
44 | exports.find = find;
45 | exports.watch = watch;
46 | exports.unwatch = unwatch;
47 |
48 | /**
49 | * Detects installed provisioning profiles.
50 | *
51 | * @param {Object} [options] - An object containing various settings.
52 | * @param {Boolean} [options.bypassCache=false] - When true, re-detects all provisioning profiles.
53 | * @param {String} [options.profileDir=~/Library/Developer/Xcode/UserData/Provisioning Profiles] - The path to search for provisioning profiles.
54 | * @param {Boolean} [options.unmanaged] - When true, excludes managed provisioning profiles.
55 | * @param {Boolean} [options.validOnly=true] - When true, only returns non-expired, valid provisioning profiles.
56 | * @param {Boolean} [options.watch=false] - If true, watches the specified provisioning profile directory for updates.
57 | * @param {Function} [callback(err, results)] - A function to call with the provisioning profile information.
58 | *
59 | * @emits module:provisioning#detected
60 | * @emits module:provisioning#error
61 | *
62 | * @returns {Handle}
63 | */
64 | function detect(options, callback) {
65 | return magik(options, callback, function (emitter, options, callback) {
66 | var files = {},
67 | validOnly = options.validOnly === undefined || options.validOnly === true,
68 | profileDirs = getExistingProvisioningProfileDirectories(options.profileDir),
69 | results = {
70 | provisioning: {
71 | profileDir: profileDirs[0],
72 | development: [],
73 | adhoc: [],
74 | enterprise: [],
75 | distribution: [],
76 | },
77 | issues: []
78 | },
79 | valid = {
80 | development: 0,
81 | adhoc: 0,
82 | enterprise: 0,
83 | distribution: 0
84 | },
85 |
86 | ppRegExp = /.*\.(mobileprovision|provisionprofile)$/;
87 |
88 |
89 | if (options.watch) {
90 | var throttleTimer = null;
91 |
92 | for (const profileDir of profileDirs) {
93 | if (!watchers[profileDir]) {
94 | watchers[profileDir] = {
95 | handle: fs.watch(profileDir, { persistent: false }, function (event, filename) {
96 | if (!ppRegExp.test(filename)) {
97 | // if it's not a provisioning profile, we don't care about it
98 | return;
99 | }
100 |
101 | var file = path.join(profileDir, filename);
102 |
103 | if (event === 'rename') {
104 | if (files[file]) {
105 | if (fs.existsSync(file)) {
106 | // change, reload the provisioning profile
107 | parseProfile(file);
108 | } else {
109 | // delete
110 | removeProfile(file);
111 | }
112 | } else {
113 | // add
114 | parseProfile(file);
115 | }
116 | } else if (event === 'change') {
117 | // updated
118 | parseProfile(file);
119 | }
120 |
121 | clearTimeout(throttleTimer);
122 |
123 | throttleTimer = setTimeout(function () {
124 | detectIssues();
125 | emitter.emit('detected', results);
126 | }, 250);
127 | }),
128 | count: 0
129 | };
130 | }
131 |
132 | watchers[profileDir].count++;
133 | }
134 | }
135 |
136 | if (cache && !options.bypassCache) {
137 | emitter.emit('detected', cache);
138 | return callback(null, cache);
139 | }
140 |
141 | function detectIssues() {
142 | results.issues = [];
143 |
144 | if (results.provisioning.development.length > 0 && !valid.development) {
145 | results.issues.push({
146 | id: 'IOS_NO_VALID_DEVELOPMENT_PROVISIONING_PROFILES',
147 | type: 'warning',
148 | message: __('Unable to find any valid iOS development provisioning profiles.') + '\n' +
149 | __('This will prevent you from building apps for testing on iOS devices.')
150 | });
151 | }
152 |
153 | if (results.provisioning.adhoc.length > 0 && !valid.adhoc) {
154 | results.issues.push({
155 | id: 'IOS_NO_VALID_ADHOC_PROVISIONING_PROFILES',
156 | type: 'warning',
157 | message: __('Unable to find any valid iOS adhoc provisioning profiles.') + '\n' +
158 | __('This will prevent you from packaging apps for adhoc distribution.')
159 | });
160 | }
161 |
162 | if (results.provisioning.distribution.length > 0 && !valid.distribution) {
163 | results.issues.push({
164 | id: 'IOS_NO_VALID_DISTRIBUTION_PROVISIONING_PROFILES',
165 | type: 'warning',
166 | message: __('Unable to find any valid iOS distribution provisioning profiles.') + '\n' +
167 | __('This will prevent you from packaging apps for AppStore distribution.')
168 | });
169 | }
170 | }
171 |
172 | function removeProfile(file) {
173 | var r = results[files[file]],
174 | i = 0,
175 | l = r.length;
176 | for (; i < l; i++) {
177 | if (r[i].file === file) {
178 | r.splice(i, 1);
179 | break;
180 | }
181 | }
182 | delete files[file];
183 | }
184 |
185 | function parseProfile(file) {
186 | if (!fs.existsSync(file)) {
187 | return;
188 | }
189 |
190 | var contents = fs.readFileSync(file).toString(),
191 | i = contents.indexOf('');
193 |
194 | if (j === -1) return;
195 |
196 | var plist = new appc.plist().parse(contents.substring(i, j + 8)),
197 | dest = 'development', // debug
198 | appPrefix = (plist.ApplicationIdentifierPrefix || []).shift(),
199 | entitlements = plist.Entitlements || {},
200 | expired = false;
201 |
202 | if (plist.ProvisionedDevices) {
203 | if (!entitlements['get-task-allow']) {
204 | dest = 'adhoc';
205 | }
206 | } else if (plist.ProvisionsAllDevices) {
207 | dest = 'enterprise';
208 | } else {
209 | dest = 'distribution'; // app store
210 | }
211 |
212 | try {
213 | if (plist.ExpirationDate) {
214 | expired = new Date(plist.ExpirationDate) < new Date;
215 | }
216 | } catch (e) {}
217 |
218 | if (!expired) {
219 | valid[dest]++;
220 | }
221 |
222 | // store which bucket the provisioning profile is in
223 | files[file] && removeProfile(file);
224 | files[file] = dest;
225 |
226 | var managed = plist.Name.indexOf('iOS Team Provisioning Profile') !== -1;
227 |
228 | if ((!validOnly || !expired) && (!options.unmanaged || !managed)) {
229 | results.provisioning[dest].push({
230 | file: file,
231 | uuid: plist.UUID,
232 | name: plist.Name,
233 | managed: managed,
234 | appPrefix: appPrefix,
235 | creationDate: plist.CreationDate,
236 | expirationDate: plist.ExpirationDate,
237 | expired: expired,
238 | certs: Array.isArray(plist.DeveloperCertificates)
239 | ? plist.DeveloperCertificates.map(function (cert) { return cert.value; })
240 | : null,
241 | devices: plist.ProvisionedDevices || null,
242 | team: plist.TeamIdentifier || null,
243 | entitlements: entitlements,
244 | // TODO: remove all of the entitlements below and just use the `entitlements` property
245 | appId: (entitlements['application-identifier'] || entitlements['com.apple.application-identifier'] || '').replace(appPrefix + '.', ''),
246 | getTaskAllow: !!entitlements['get-task-allow'],
247 | apsEnvironment: entitlements['aps-environment'] || ''
248 | });
249 | }
250 | }
251 |
252 | for (const profileDir of profileDirs) {
253 | fs.readdirSync(profileDir).forEach(function (name) {
254 | ppRegExp.test(name) && parseProfile(path.join(profileDir, name));
255 | });
256 | }
257 |
258 | detectIssues();
259 | cache = results;
260 | emitter.emit('detected', results);
261 | return callback(null, results);
262 | });
263 | };
264 |
265 | /**
266 | * Finds all provisioning profiles that match the specified developer cert name
267 | * and iOS device UDID.
268 | *
269 | * @param {Object} [options] - An object containing various settings.
270 | * @param {String} [options.appId] - The app identifier (com.domain.app) to filter by.
271 | * @param {Object|Array