termux/termux-app · critical · RuntimeException
Malformed symlink line:
Error message
Malformed symlink line:
What it means
Thrown during Termux bootstrap installation while parsing the SYMLINKS.txt entry inside the bootstrap zip. Each line in SYMLINKS.txt must contain exactly one '←' character so that line.split("←") yields exactly two parts (oldPath, newPath). If a line has zero, two, or more '←' separators the split produces != 2 parts and this RuntimeException is raised, aborting the whole prefix setup.
Source
Thrown at app/src/main/java/com/termux/app/TermuxInstaller.java:169
return;
}
Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\".");
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final byte[] zipBytes = loadZipBytes();
try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) {
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
String line;
while ((line = symlinksReader.readLine()) != null) {
String[] parts = line.split("←");
if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line);
String oldPath = parts[0];
String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
error = ensureDirectoryExists(new File(newPath).getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));
return;
}
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName);
boolean isDirectory = zipEntry.isDirectory();
error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
if (error != null) {
showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));View on GitHub (pinned to 3df69d1da1)
Solutions
- Inspect the failing line value in the exception message; it is appended verbatim after 'Malformed symlink line: '.
- Open the bootstrap zip and examine SYMLINKS.txt; ensure every non-empty line has exactly one '←' separating the link target from the install-relative path.
- Regenerate the bootstrap zip with a known-good termux-packages build so the delimiter matches, or strip/ignore blank lines in the parser.
- If maintaining a fork, change the parser's expected delimiter consistently with how SYMLINKS.txt is generated.
Example fix
// before
String[] parts = line.split("←");
if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line);
// after (tolerate blank lines, validate delimiter)
if (line.isEmpty()) continue;
String[] parts = line.split("←");
if (parts.length != 2)
throw new RuntimeException("Malformed symlink line (expected one '←'): " + line); Defensive patterns
Strategy: validation
Validate before calling
// Validate each SYMLINKS.txt line before splitting
if (line.isEmpty() || line.startsWith("#")) continue;
long arrowCount = line.chars().filter(c -> c == '\u2190').count();
if (arrowCount != 1) {
throw new IOException("Expected exactly one '\u2190' in line: " + line);
}
String[] parts = line.split("\u2190");
assert parts.length == 2; Try / catch
// Wrap bootstrap setup so a parse failure surfaces a user-facing error
try {
setupBootstrap(activity);
} catch (RuntimeException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Malformed symlink line")) {
showErrorDialog("Bootstrap SYMLINKS.txt is corrupt: " + e.getMessage());
} else throw e;
} Prevention
- Only use bootstrap zips produced by the official termux-packages build.
- Treat SYMLINKS.txt as machine-generated; never hand-edit delimiter lines.
- Add a unit test that parses a known-good SYMLINKS.txt fixture to catch format regressions.
When it happens
Trigger: A bootstrap zip ships a SYMLINKS.txt entry where a line is blank, contains no '←' (e.g. uses '->' or ASCII arrow), contains multiple '←', or is a stray comment/header line. Also triggered if the file uses a different delimiter than the code expects or has CRLF corruption that shifts parsing.
Common situations: Custom/modified bootstrap zip with a hand-edited SYMLINKS.txt; bootstrap generated by a different toolchain that uses '->' instead of '←'; a corrupted download that garbled delimiter bytes; end-of-file trailing blank line that is not skipped.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- No SYMLINKS.txt encountered
- Moving termux prefix staging to prefix directory failed
- Unsupported TERMUX_APP_PACKAGE_VARIANT "%s"
- Unsupported TERMUX_APP_PACKAGE_MANAGER "%s" with variant "%s
- Invalid path:
AI-assisted analysis of termux/termux-app@3df69d1da1 (2026-08-13).
Data as JSON: /api/errors/3a016200c4731464.
Report an issue: GitHub.