Commit d8259fc2c7
d8259fc2c727995f7e45dd89b04231a66a65862a
parent: f18103132a
Verified · cmc
cmc <hello@cleberg.net> · 2026-10-08 03:31 UTC
Run Emacs export with Babel's PATH and stop it on cancel
Export uses BabelRunner.environment, which now also adds
/Library/TeX/texbin. Cancelling an export or a table recalculation
terminates Emacs; Task.sleep used to throw before the process was
terminated.
Layout: unified · split
Sources/OrgApp/BabelRunner.swift
+1 −1
| @@ -12,7 +12,7 @@ public enum BabelRunner { |
| 12 | 12 | /// PATH with the usual install locations, which an app launched from the Dock lacks. |
| 13 | 13 | public static var environment: [String: String] { |
| 14 | 14 | var environment = ProcessInfo.processInfo.environment |
| 15 | | let extra = ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"] |
| 15 | let extra = ["/opt/homebrew/bin", "/usr/local/bin", "/Library/TeX/texbin", "/usr/bin", "/bin"] |
| 16 | 16 | let path = (environment["PATH"] ?? "").split(separator: ":").map(String.init) |
| 17 | 17 | environment["PATH"] = (path + extra.filter { !path.contains($0) }).joined(separator: ":") |
| 18 | 18 | return environment |
Sources/OrgApp/EmacsRunner.swift
+8 −2
| @@ -67,7 +67,7 @@ public enum EmacsRunner { |
| 67 | 67 | process.terminate() |
| 68 | 68 | throw Failure(description: "Emacs took too long to recalculate the table.") |
| 69 | 69 | } |
| 70 | | try await Task.sleep(for: .milliseconds(50)) |
| 70 | try? await Task.sleep(for: .milliseconds(50)) |
| 71 | 71 | if Task.isCancelled { |
| 72 | 72 | process.terminate() |
| 73 | 73 | throw CancellationError() |
| @@ -113,6 +113,8 @@ public enum EmacsRunner { |
| 113 | 113 | process.executableURL = emacs |
| 114 | 114 | process.arguments = ["-Q", "--batch", "-l", script.path] |
| 115 | 115 | process.currentDirectoryURL = URL(fileURLWithPath: path).deletingLastPathComponent() |
| 116 | // Babel's PATH, so a Dock-launched app's Emacs finds latexmk and pdflatex. |
| 117 | process.environment = BabelRunner.environment |
| 116 | 118 | let errors = Pipe() |
| 117 | 119 | process.standardError = errors |
| 118 | 120 | process.standardOutput = FileHandle.nullDevice |
| @@ -123,7 +125,11 @@ public enum EmacsRunner { |
| 123 | 125 | process.terminate() |
| 124 | 126 | throw Failure(description: "Emacs took too long to export.") |
| 125 | 127 | } |
| 126 | | try await Task.sleep(for: .milliseconds(100)) |
| 128 | try? await Task.sleep(for: .milliseconds(100)) |
| 129 | if Task.isCancelled { |
| 130 | process.terminate() |
| 131 | throw CancellationError() |
| 132 | } |
| 127 | 133 | } |
| 128 | 134 | guard let written = try? String(contentsOf: output, encoding: .utf8) else { |
| 129 | 135 | let log = String(decoding: errors.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self) |
Tests/OrgAppTests/AppTests.swift
+32
| @@ -783,6 +783,38 @@ struct EmacsExportTests { |
| 783 | 783 | #expect(text.uppercased().contains("IN THE BUFFER") && text.contains("Body."), "\(text)") |
| 784 | 784 | #expect(try String(contentsOf: file, encoding: .utf8) == "#+TITLE: On disk\n") |
| 785 | 785 | } |
| 786 | |
| 787 | @Test(.enabled(if: EmacsRunner.executable() != nil && ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) |
| 788 | func exportUsesBabelPath() async throws { |
| 789 | let notes = try Folder() |
| 790 | let file = try notes.write("doc.org", "") |
| 791 | let function = #"(lambda () (with-temp-file "path.txt" (insert (getenv "PATH"))) "path.txt")"# |
| 792 | let written = try await EmacsRunner.export(text: "", path: file.path, function: function) |
| 793 | #expect(try String(contentsOfFile: written, encoding: .utf8) == BabelRunner.environment["PATH"]) |
| 794 | } |
| 795 | |
| 796 | @Test(.enabled(if: EmacsRunner.executable() != nil && ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil)) |
| 797 | func cancellingStopsTheExport() async throws { |
| 798 | let notes = try Folder() |
| 799 | let file = try notes.write("doc.org", "") |
| 800 | let pidFile = notes.url.appendingPathComponent("pid") |
| 801 | let function = #"(lambda () (with-temp-file "pid" (insert (number-to-string (emacs-pid)))) (sleep-for 60) "x")"# |
| 802 | let export = Task { try await EmacsRunner.export(text: "", path: file.path, function: function) } |
| 803 | var pid: pid_t? |
| 804 | for _ in 0..<200 where pid == nil { |
| 805 | try await Task.sleep(for: .milliseconds(50)) |
| 806 | pid = (try? String(contentsOf: pidFile, encoding: .utf8)).flatMap { pid_t($0) } |
| 807 | } |
| 808 | let started = try #require(pid) |
| 809 | export.cancel() |
| 810 | await #expect(throws: CancellationError.self) { try await export.value } |
| 811 | var alive = true |
| 812 | for _ in 0..<100 where alive { |
| 813 | alive = kill(started, 0) == 0 |
| 814 | if alive { try await Task.sleep(for: .milliseconds(50)) } |
| 815 | } |
| 816 | #expect(!alive) |
| 817 | } |
| 786 | 818 | } |
| 787 | 819 | |
| 788 | 820 | @MainActor |