Skip to content

Commit 9287b16

Browse files
committed
feat: Do proper up-to-date checks on requirements and python installation
1 parent 15dfd49 commit 9287b16

1 file changed

Lines changed: 80 additions & 39 deletions

File tree

src/main/java/io/github/berstanio/pymobiledevice3/venv/PyInstallationHandler.java

Lines changed: 80 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,30 @@
88
import java.io.File;
99
import java.io.IOException;
1010
import java.io.InputStream;
11+
import java.io.Reader;
12+
import java.io.Writer;
1113
import java.net.MalformedURLException;
1214
import java.net.URL;
1315
import java.nio.charset.StandardCharsets;
1416
import java.nio.file.Files;
1517
import java.nio.file.StandardCopyOption;
18+
import java.security.MessageDigest;
19+
import java.security.NoSuchAlgorithmException;
20+
import java.util.Base64;
21+
import java.util.Properties;
1622

1723
public class PyInstallationHandler {
1824

1925
private static final boolean DEBUG = System.getProperty("java.pymobiledevice3.debug") != null;
2026

2127
private static final String HANDLER_NAME = "handler.py";
2228
private static final String REQUIREMENTS_NAME = "requirements.txt";
23-
private static final String READY_MARKER = ".ready";
2429
private static final String PYTHON_DIR = "python";
2530

31+
private static final String READY_MARKER = ".ready";
32+
private static final String READY_PYTHON = "python";
33+
private static final String READY_REQUIREMENTS = "requirements";
34+
2635
private static final String PBS_RELEASE = "20260623";
2736
private static final String PBS_PYTHON_VERSION = "3.14.6";
2837
private static final String PBS_FLAVOR = "install_only_stripped";
@@ -41,68 +50,100 @@ public static PyInstallation install(File directory) {
4150
}
4251

4352
public static PyInstallation install(File directory, File ipcBaseDirectory) {
44-
if (isReady(directory))
45-
return toInstallation(directory);
46-
47-
prepareDirectory(directory);
48-
4953
try {
50-
Files.copy(ipcBaseDirectory.toPath().resolve(HANDLER_NAME), directory.toPath().resolve(HANDLER_NAME), StandardCopyOption.REPLACE_EXISTING);
51-
Files.copy(ipcBaseDirectory.toPath().resolve(REQUIREMENTS_NAME), directory.toPath().resolve(REQUIREMENTS_NAME), StandardCopyOption.REPLACE_EXISTING);
52-
} catch (IOException e) {
53-
throw new RuntimeException("Failed to copy files", e);
54+
return install(directory,
55+
new File(ipcBaseDirectory, HANDLER_NAME).toURI().toURL(),
56+
new File(ipcBaseDirectory, REQUIREMENTS_NAME).toURI().toURL());
57+
} catch (MalformedURLException e) {
58+
throw new RuntimeException("Failed to resolve IPC URL", e);
5459
}
55-
56-
return installInternal(directory);
5760
}
5861

5962
public static PyInstallation install(File directory, URL handlerFile, URL requirementsFile) {
60-
if (isReady(directory))
61-
return toInstallation(directory);
62-
6363
prepareDirectory(directory);
64+
refresh(new File(directory, HANDLER_NAME), handlerFile);
65+
refresh(new File(directory, REQUIREMENTS_NAME), requirementsFile);
66+
return installInternal(directory);
67+
}
6468

65-
try {
66-
try (InputStream in = handlerFile.openStream()) {
67-
Files.copy(in, directory.toPath().resolve(HANDLER_NAME), StandardCopyOption.REPLACE_EXISTING);
68-
}
69-
try (InputStream in = requirementsFile.openStream()) {
70-
Files.copy(in, directory.toPath().resolve(REQUIREMENTS_NAME), StandardCopyOption.REPLACE_EXISTING);
71-
}
69+
private static void refresh(File target, URL source) {
70+
try (InputStream in = source.openStream()) {
71+
Files.copy(in, target.toPath(), StandardCopyOption.REPLACE_EXISTING);
7272
} catch (IOException e) {
73-
throw new RuntimeException("Failed to download files", e);
73+
// Source unreachable (e.g. offline restart): reuse the previously fetched copy.
74+
if (!target.exists())
75+
throw new RuntimeException("Failed to fetch " + target.getName(), e);
7476
}
75-
76-
return installInternal(directory);
7777
}
7878

7979
private static void prepareDirectory(File directory) {
80-
deleteRecursively(directory);
8180
directory.mkdirs();
8281
if (!directory.exists() || !directory.isDirectory())
8382
throw new IllegalArgumentException(directory.getAbsolutePath() + " does not exist or is not a directory");
8483
}
8584

86-
private static boolean isReady(File directory) {
87-
return new File(directory, READY_MARKER).exists() && pythonExecutable(directory).exists();
88-
}
89-
9085
private static PyInstallation installInternal(File directory) {
91-
File pythonHome = new File(directory, PYTHON_DIR);
92-
if (pythonHome.exists() && !deleteRecursively(pythonHome))
93-
throw new IllegalStateException("Failed to remove stale interpreter at " + pythonHome.getAbsolutePath());
86+
String urlHash = hash(standaloneUrl().getBytes(StandardCharsets.UTF_8));
87+
String reqHash = hash(readBytes(new File(directory, REQUIREMENTS_NAME)));
88+
89+
Properties ready = loadReady(directory);
90+
boolean interpreterReady = urlHash.equals(ready.getProperty(READY_PYTHON))
91+
&& pythonExecutable(directory).exists();
92+
93+
if (!interpreterReady) {
94+
File pythonHome = new File(directory, PYTHON_DIR);
95+
if (pythonHome.exists() && !deleteRecursively(pythonHome))
96+
throw new IllegalStateException("Failed to remove stale interpreter at " + pythonHome.getAbsolutePath());
97+
downloadAndExtractInterpreter(directory);
98+
installRequirements(directory);
99+
100+
ready.setProperty(READY_PYTHON, urlHash);
101+
ready.setProperty(READY_REQUIREMENTS, reqHash);
102+
storeReady(directory, ready);
103+
} else if (!reqHash.equals(ready.getProperty(READY_REQUIREMENTS))) {
104+
installRequirements(directory);
105+
ready.setProperty(READY_REQUIREMENTS, reqHash);
106+
storeReady(directory, ready);
107+
}
108+
109+
return toInstallation(directory);
110+
}
94111

95-
downloadAndExtractInterpreter(directory);
96-
installRequirements(directory);
112+
private static Properties loadReady(File directory) {
113+
Properties props = new Properties();
114+
File f = new File(directory, READY_MARKER);
115+
if (f.exists()) {
116+
try (Reader r = Files.newBufferedReader(f.toPath(), StandardCharsets.UTF_8)) {
117+
props.load(r);
118+
} catch (IOException e) {
119+
return new Properties();
120+
}
121+
}
122+
return props;
123+
}
97124

98-
try {
99-
Files.deleteIfExists(new File(directory, READY_MARKER).toPath());
100-
Files.createFile(new File(directory, READY_MARKER).toPath());
125+
private static void storeReady(File directory, Properties props) {
126+
try (Writer w = Files.newBufferedWriter(new File(directory, READY_MARKER).toPath(), StandardCharsets.UTF_8)) {
127+
props.store(w, null);
101128
} catch (IOException e) {
102129
throw new IllegalStateException("Failed to write ready marker in " + directory.getAbsolutePath(), e);
103130
}
131+
}
104132

105-
return toInstallation(directory);
133+
private static String hash(byte[] bytes) {
134+
try {
135+
return Base64.getUrlEncoder().withoutPadding().encodeToString(MessageDigest.getInstance("SHA-256").digest(bytes));
136+
} catch (NoSuchAlgorithmException e) {
137+
throw new IllegalStateException(e);
138+
}
139+
}
140+
141+
private static byte[] readBytes(File f) {
142+
try {
143+
return Files.readAllBytes(f.toPath());
144+
} catch (IOException e) {
145+
throw new IllegalStateException("Failed to read " + f.getAbsolutePath(), e);
146+
}
106147
}
107148

108149
private static void downloadAndExtractInterpreter(File directory) {

0 commit comments

Comments
 (0)