Skip to content

Commit 38c41c2

Browse files
author
José Pereda
authored
#142 Refactor Deploy, check for libraries required by ios-deploy (#163)
close #142
1 parent c0e812b commit 38c41c2

File tree

10 files changed

+197
-641
lines changed

10 files changed

+197
-641
lines changed

build.gradle

-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ test {
2424

2525
dependencies {
2626
implementation 'com.googlecode.plist:dd-plist:1.22'
27-
implementation 'com.github.jnr:jnr-ffi:2.1.10'
2827
implementation 'org.bouncycastle:bcpkix-jdk15on:1.49'
2928

3029
testImplementation gradleTestKit()

external-licenses.md

+1-41
Original file line numberDiff line numberDiff line change
@@ -32,44 +32,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3232
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3333
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3434
SOFTWARE.
35-
---------------------------------------------------------------------------
36-
37-
jnr-ffi
38-
39-
https://github.com/jnr/jnr-ffi/blob/master/LICENSE
40-
41-
Licensed under the Apache License, Version 2.0 (the "License");
42-
you may not use this file except in compliance with the License.
43-
You may obtain a copy of the License at
44-
45-
http://www.apache.org/licenses/LICENSE-2.0
46-
47-
Unless required by applicable law or agreed to in writing, software
48-
distributed under the License is distributed on an "AS IS" BASIS,
49-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
50-
See the License for the specific language governing permissions and
51-
limitations under the License.
52-
53-
54-
Alternatively, you can redistribute it and/or modify it under
55-
the terms of the GNU Lesser General Public License as published by
56-
the Free Software Foundation, either version 3 of the License, or
57-
(at your option) any later version.
58-
59-
This code is distributed in the hope that it will be useful, but WITHOUT
60-
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
61-
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
62-
version 3 for more details.
63-
64-
You should have received a copy of the GNU Lesser General Public License
65-
version 3 along with this work. If not, see <http://www.gnu.org/licenses/>.
66-
---------------------------------------------------------------------------
67-
68-
libimobiledevice
69-
70-
https://github.com/libimobiledevice/libimobiledevice/blob/master/COPYING
71-
72-
The libimobiledevice library is licensed under the GNU Lesser General Public Library, version 2.1.
73-
74-
You can find a copy of this license at https://www.gnu.org/licenses/lgpl-2.1.en.html
75-
---------------------------------------------------------------------------
35+
---------------------------------------------------------------------------

src/main/java/com/gluonhq/substrate/target/IosTargetConfiguration.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,19 @@ List<String> getTargetSpecificNativeLibsFlags(Path libPath, List<String> libs) {
166166

167167
@Override
168168
public boolean runUntilEnd() throws IOException, InterruptedException {
169-
Deploy.addDebugSymbolInfo(paths.getAppPath(), projectConfiguration.getAppName());
169+
if (!isSimulator() && projectConfiguration.getIosSigningConfiguration().isSkipSigning()) {
170+
// without signing, app can't be deployed
171+
return true;
172+
}
173+
Deploy deploy = new Deploy();
174+
deploy.addDebugSymbolInfo(paths.getAppPath(), projectConfiguration.getAppName());
170175
String appPath = paths.getAppPath().resolve(projectConfiguration.getAppName() + ".app").toString();
171176
if (isSimulator()) {
172177
// TODO: launchOnSimulator(appPath);
173178
return false;
174-
} else if (!projectConfiguration.getIosSigningConfiguration().isSkipSigning()) {
175-
return Deploy.install(appPath);
179+
} else {
180+
return deploy.install(appPath);
176181
}
177-
return true;
178182
}
179183

180184
@Override

src/main/java/com/gluonhq/substrate/util/ProcessRunner.java

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ private Process setupProcess(String processName, File directory) throws IOExcept
140140
pb.directory(directory);
141141
}
142142
map.forEach((k, v) -> pb.environment().put(k, v));
143+
answer.setLength(0);
143144
Logger.logDebug("Start process " + processName + "...");
144145
return pb.start();
145146
}

src/main/java/com/gluonhq/substrate/util/ios/Deploy.java

+169-39
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@
3636
import java.nio.file.Path;
3737
import java.time.LocalDateTime;
3838
import java.time.format.DateTimeFormatter;
39+
import java.util.ArrayList;
40+
import java.util.Arrays;
41+
import java.util.HashMap;
3942
import java.util.List;
43+
import java.util.Map;
4044
import java.util.Objects;
4145
import java.util.stream.Collectors;
4246

@@ -45,69 +49,136 @@
4549
public class Deploy {
4650

4751
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
48-
private static MobileDeviceBridge bridge;
52+
private static final List<String> LIBIMOBILEDEVICE_DEPENDENCIES = Arrays.asList(
53+
"libssl", "libcrypto", "libusbmuxd", "libplist");
54+
55+
private Path iosDeployPath;
56+
57+
public Deploy() throws IOException, InterruptedException {
58+
checkPrerequisites();
59+
}
60+
61+
public Path getIosDeployPath() {
62+
return iosDeployPath;
63+
}
64+
65+
private void checkPrerequisites() throws IOException, InterruptedException {
66+
iosDeployPath = null;
4967

50-
public static Path getIOSDeployPath() throws IOException, InterruptedException {
5168
// Check for Homebrew installed
5269
String response = ProcessRunner.runProcessForSingleOutput("check brew","which", "brew");
53-
if (response == null || response.isEmpty()) {
70+
if (response == null || response.isEmpty() || !Files.exists(Path.of(response))) {
5471
Logger.logSevere("Homebrew not found");
5572
throw new RuntimeException("Open a terminal and run the following command to install Homebrew: \n\n" +
5673
"ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"");
5774
}
5875
Logger.logDebug("Brew found at " + response);
5976

77+
// Check if dependencies of libimobiledevice are installed and retrieve linked versions
78+
Map<String, List<String>> map = new HashMap<>();
79+
for (String nameLib : LIBIMOBILEDEVICE_DEPENDENCIES) {
80+
List<String> pathLibs = checkDependencyPaths(nameLib);
81+
List<String> linkLibs = checkDependencyLinks(nameLib, pathLibs);
82+
map.put(nameLib, linkLibs);
83+
}
84+
85+
// Check for libimobiledevice installed
86+
List<String> libiPath = checkDependencyPaths("libimobiledevice");
87+
if (libiPath == null || libiPath.isEmpty() || !Files.exists(Path.of(libiPath.get(0)))) {
88+
Logger.logSevere("Error finding libimobiledevice.dylib");
89+
return;
90+
}
91+
92+
ProcessRunner runner = new ProcessRunner("otool", "-L", libiPath.get(0));
93+
if (runner.runProcess("otool") == 0) {
94+
for (String key : map.keySet()) {
95+
if (runner.getResponses().stream()
96+
.noneMatch(link -> map.get(key).stream().anyMatch(link::contains))) {
97+
Logger.logSevere("Error: there is a mismatch in the dependency (" + key + ") required by libimobiledevice.dylib: " + map.get(key) + "is required but it wasn't found");
98+
throw new RuntimeException("Open a terminal and run the following command to reinstall the required libraries: \n\n" +
99+
"brew reinstall " + key);
100+
}
101+
}
102+
}
103+
60104
// Check for ios-deploy installed
61105
response = ProcessRunner.runProcessForSingleOutput("check ios-deploy","which", "ios-deploy");
62-
if (response == null || response.isEmpty()) {
63-
Logger.logSevere("ios-deploy not found. It will be installed now");
64-
ProcessRunner runner = new ProcessRunner("brew", "install", "ios-deploy");
65-
if (runner.runProcess("ios-deploy") == 0) {
66-
Logger.logDebug("ios-deploy installed");
67-
return getIOSDeployPath();
68-
} else {
69-
Logger.logDebug("Error installing ios-deploy");
70-
return null;
106+
if (response == null || response.isEmpty() || !Files.exists(Path.of(response))) {
107+
if (installIOSDeploy()) {
108+
checkPrerequisites();
71109
}
72110
} else {
73-
Logger.logDebug("ios-deploy found at " + response);
111+
// Check for ios-deploy version installed (it should be 1.10+)
112+
String version = ProcessRunner.runProcessForSingleOutput("ios-deploy version","ios-deploy", "-V");
113+
if (version != null && !version.isEmpty() && (version.startsWith("1.8") || version.startsWith("1.9"))) {
114+
Logger.logDebug("ios-deploy version (" + version + ") is outdated");
115+
uninstallIOSDeploy();
116+
if (installIOSDeploy()) {
117+
checkPrerequisites();
118+
}
119+
} else {
120+
Logger.logDebug("ios-deploy found at " + response);
121+
iosDeployPath = Path.of(response);
122+
}
74123
}
75-
return Path.of(response);
76124
}
77125

78-
public static String[] connectedDevices() {
79-
if (bridge == null) {
80-
bridge = MobileDeviceBridge.instance;
126+
private String[] connectedDevices() throws IOException, InterruptedException {
127+
if (iosDeployPath == null) {
128+
return new String[] {};
81129
}
82-
83-
return bridge.getDeviceIds();
130+
ProcessRunner runner = new ProcessRunner("ios-deploy", "-c");
131+
if (!runner.runTimedProcess("connected devices", 10L)) {
132+
Logger.logSevere("Error finding connected devices");
133+
return new String[] {};
134+
}
135+
List<String> devices = runner.getResponses();
136+
return devices.stream()
137+
.filter(line -> line.startsWith("[....] Found"))
138+
.map(line -> line.substring("[....] Found ".length()).split("\\s")[0])
139+
.peek(id -> Logger.logDebug("ID found: " + id))
140+
.toArray(String[]::new);
84141
}
85142

86-
public static boolean install(String app) throws IOException, InterruptedException {
87-
Path deploy = getIOSDeployPath();
88-
if (deploy != null) {
89-
String[] devices = Deploy.connectedDevices();
90-
if (devices == null || devices.length == 0) {
91-
Logger.logSevere("No iOS devices connected to this system. Exit install procedure");
92-
return false;
93-
}
94-
if (devices.length > 1) {
95-
Logger.logSevere("Multiple iOS devices connected to this system: " + String.join(", ", devices ) + ". We'll use the first one.");
96-
}
97-
String deviceId = devices[0];
143+
public boolean install(String app) throws IOException, InterruptedException {
144+
if (iosDeployPath == null) {
145+
Logger.logSevere("Error: ios-deploy was not found");
146+
return false;
147+
}
98148

99-
ProcessRunner runner = new ProcessRunner(deploy.toString(),
100-
"--id", deviceId, "--bundle", app, "--no-wifi", "--debug", "--noninteractive");
101-
runner.addToEnv("PATH", "/usr/bin/:$PATH");
102-
runner.setInfo(true);
149+
String[] devices = connectedDevices();
150+
if (devices == null || devices.length == 0) {
151+
Logger.logInfo("No iOS devices connected to this system. Exit install procedure");
152+
return false;
153+
}
154+
if (devices.length > 1) {
155+
Logger.logInfo("Multiple iOS devices connected to this system: " + String.join(", ", devices ) + ". We'll use the first one.");
156+
}
157+
String deviceId = devices[0];
158+
159+
ProcessRunner runner = new ProcessRunner(iosDeployPath.toString(),
160+
"--id", deviceId, "--bundle", app, "--no-wifi", "--debug", "--noninteractive");
161+
runner.addToEnv("PATH", "/usr/bin/:$PATH");
162+
runner.setInfo(true);
163+
boolean keepTrying = true;
164+
while (keepTrying) {
165+
keepTrying = false;
103166
boolean result = runner.runTimedProcess("run", 60);
104167
Logger.logInfo("result = " + result);
105-
return result;
168+
if (result) {
169+
if (runner.getResponses().stream().anyMatch(l -> "Error: The device is locked.".equals(l))) {
170+
Logger.logInfo("Device locked! Please, unlock and press ENTER to try again");
171+
System.in.read();
172+
keepTrying = true;
173+
}
174+
} else {
175+
return false;
176+
}
106177
}
107-
return false;
178+
return true;
108179
}
109180

110-
public static void addDebugSymbolInfo(Path appPath, String appName) throws IOException, InterruptedException {
181+
public void addDebugSymbolInfo(Path appPath, String appName) throws IOException, InterruptedException {
111182
Path applicationPath = appPath.resolve(appName + ".app");
112183
Path debugSymbolsPath = Path.of(applicationPath.toString() + ".dSYM");
113184
if (Files.exists(debugSymbolsPath)) {
@@ -124,7 +195,7 @@ public static void addDebugSymbolInfo(Path appPath, String appName) throws IOExc
124195
}
125196
}
126197

127-
private static void copyAppToProducts(Path debugSymbolsPath, Path executablePath, String appName) throws IOException {
198+
private void copyAppToProducts(Path debugSymbolsPath, Path executablePath, String appName) throws IOException {
128199
if (Files.exists(XCODE_PRODUCTS_PATH)) {
129200
List<Path> oldAppsPaths = Files.walk(XCODE_PRODUCTS_PATH, 1)
130201
.filter(Objects::nonNull)
@@ -148,4 +219,63 @@ private static void copyAppToProducts(Path debugSymbolsPath, Path executablePath
148219
Files.createDirectories(productDebugSymbolsPath);
149220
FileOps.copyDirectory(debugSymbolsPath, productDebugSymbolsPath);
150221
}
222+
223+
private List<String> checkDependencyPaths(String nameLib) throws IOException, InterruptedException {
224+
ProcessRunner runner = new ProcessRunner("/bin/sh", "-c", "find $(brew --cellar) -name " + nameLib + ".dylib");
225+
if (runner.runProcess(nameLib) != 0) {
226+
Logger.logDebug("Error finding " + nameLib);
227+
return new ArrayList<>();
228+
}
229+
return runner.getResponses().stream()
230+
.map(libPath -> {
231+
Logger.logDebug("lib " + nameLib + " found at " + libPath);
232+
if (libPath == null || libPath.isEmpty() || !Files.exists(Path.of(libPath))) {
233+
Logger.logSevere("Error finding " + nameLib + ".dylib");
234+
throw new RuntimeException("Open a terminal and run the following command to install " + nameLib + ".dylib: \n\n" +
235+
"brew install --HEAD " + nameLib);
236+
}
237+
Logger.logDebug(nameLib + ".dylib found in: " + libPath);
238+
return libPath;
239+
})
240+
.collect(Collectors.toList());
241+
}
242+
243+
private List<String> checkDependencyLinks(String nameLib, List<String> libPaths) throws IOException, InterruptedException {
244+
List<String> libLinks = new ArrayList<>();
245+
for (String libPath : libPaths) {
246+
// retrieve name of linked library
247+
String linkedLib = ProcessRunner.runProcessForSingleOutput("readlink " + nameLib, "readlink", libPath);
248+
Logger.logDebug(nameLib + ".dylib link of: " + linkedLib);
249+
if (linkedLib == null || linkedLib.isEmpty()) {
250+
throw new RuntimeException("Error finding " + nameLib + ".dylib version");
251+
}
252+
libLinks.add(linkedLib);
253+
}
254+
return libLinks;
255+
}
256+
257+
private boolean uninstallIOSDeploy() throws IOException, InterruptedException {
258+
ProcessRunner runner = new ProcessRunner("brew", "unlink", "ios-deploy");
259+
if (runner.runProcess("ios-deploy unlink") == 0) {
260+
Logger.logDebug("ios-deploy unlinked");
261+
}
262+
Logger.logDebug("Uninstalling ios-deploy");
263+
runner = new ProcessRunner("brew", "uninstall", "ios-deploy");
264+
if (runner.runProcess("ios-deploy uninstall") == 0) {
265+
Logger.logDebug("ios-deploy uninstalled");
266+
return true;
267+
}
268+
return false;
269+
}
270+
271+
private boolean installIOSDeploy() throws IOException, InterruptedException {
272+
Logger.logInfo("ios-deploy not found. It will be installed now");
273+
274+
ProcessRunner runner = new ProcessRunner("brew", "install", "--HEAD", "ios-deploy");
275+
if (runner.runProcess("ios-deploy") == 0) {
276+
Logger.logDebug("ios-deploy installed");
277+
return true;
278+
}
279+
throw new RuntimeException("Error installing ios-deploy. See detailed message above on how to proceed. Then try to deploy again");
280+
}
151281
}

0 commit comments

Comments
 (0)