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 | /// PATH with the usual install locations, which an app launched from the Dock lacks. |
12 | /// PATH with the usual install locations, which an app launched from the Dock lacks. |
| 13 | public static var environment: [String: String] { |
13 | public static var environment: [String: String] { |
| 14 | var environment = ProcessInfo.processInfo.environment |
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 | let path = (environment["PATH"] ?? "").split(separator: ":").map(String.init) |
16 | let path = (environment["PATH"] ?? "").split(separator: ":").map(String.init) |
| 17 | environment["PATH"] = (path + extra.filter { !path.contains($0) }).joined(separator: ":") |
17 | environment["PATH"] = (path + extra.filter { !path.contains($0) }).joined(separator: ":") |
| 18 | return environment |
18 | return environment |
Sources/OrgApp/EmacsRunner.swift
+8 −2
| @@ -67,7 +67,7 @@ public enum EmacsRunner { |
| 67 | process.terminate() |
67 | process.terminate() |
| 68 | throw Failure(description: "Emacs took too long to recalculate the table.") |
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 | if Task.isCancelled { |
71 | if Task.isCancelled { |
| 72 | process.terminate() |
72 | process.terminate() |
| 73 | throw CancellationError() |
73 | throw CancellationError() |
| @@ -113,6 +113,8 @@ public enum EmacsRunner { |
| 113 | process.executableURL = emacs |
113 | process.executableURL = emacs |
| 114 | process.arguments = ["-Q", "--batch", "-l", script.path] |
114 | process.arguments = ["-Q", "--batch", "-l", script.path] |
| 115 | process.currentDirectoryURL = URL(fileURLWithPath: path).deletingLastPathComponent() |
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 | let errors = Pipe() |
118 | let errors = Pipe() |
| 117 | process.standardError = errors |
119 | process.standardError = errors |
| 118 | process.standardOutput = FileHandle.nullDevice |
120 | process.standardOutput = FileHandle.nullDevice |
| @@ -123,7 +125,11 @@ public enum EmacsRunner { |
| 123 | process.terminate() |
125 | process.terminate() |
| 124 | throw Failure(description: "Emacs took too long to export.") |
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 | guard let written = try? String(contentsOf: output, encoding: .utf8) else { |
134 | guard let written = try? String(contentsOf: output, encoding: .utf8) else { |
| 129 | let log = String(decoding: errors.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self) |
135 | let log = String(decoding: errors.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self) |
Tests/OrgAppTests/AppTests.swift
+32
| @@ -783,6 +783,38 @@ struct EmacsExportTests { |
| 783 | #expect(text.uppercased().contains("IN THE BUFFER") && text.contains("Body."), "\(text)") |
783 | #expect(text.uppercased().contains("IN THE BUFFER") && text.contains("Body."), "\(text)") |
| 784 | #expect(try String(contentsOf: file, encoding: .utf8) == "#+TITLE: On disk\n") |
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 | @MainActor |
820 | @MainActor |