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

  1. Inspect the failing line value in the exception message; it is appended verbatim after 'Malformed symlink line: '.
  2. 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.
  3. Regenerate the bootstrap zip with a known-good termux-packages build so the delimiter matches, or strip/ignore blank lines in the parser.
  4. 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

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

Related errors


AI-assisted analysis of termux/termux-app@3df69d1da1 (2026-08-13). Data as JSON: /api/errors/3a016200c4731464. Report an issue: GitHub.