Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions app/release/burrito_patches.exs
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,44 @@ defmodule LinearCli.Release.BurritoPatches do

@doc false
def patch_source!(source) when is_binary(source) do
cond do
contains_both?(source, @tty_aware_proxy, @conditional_error_join) ->
source

contains_both?(source, @always_proxy, @unconditional_error_join) ->
source
|> String.replace(@always_proxy, @tty_aware_proxy)
|> String.replace(@unconditional_error_join, @conditional_error_join)

true ->
raise """
Burrito's stdout launcher no longer matches the expected source. Refusing to
build without checking whether the terminal inheritance fix is still needed.
This temporary patch tracks #{@upstream_pr}.
"""
end
newline = newline_style(source)
normalized_source = normalize_newlines(source)
always_proxy = normalize_newlines(@always_proxy)
tty_aware_proxy = normalize_newlines(@tty_aware_proxy)
unconditional_error_join = normalize_newlines(@unconditional_error_join)
conditional_error_join = normalize_newlines(@conditional_error_join)

patched =
cond do
contains_both?(normalized_source, tty_aware_proxy, conditional_error_join) ->
normalized_source

contains_both?(normalized_source, always_proxy, unconditional_error_join) ->
normalized_source
|> String.replace(always_proxy, tty_aware_proxy)
|> String.replace(unconditional_error_join, conditional_error_join)

true ->
raise """
Burrito's stdout launcher no longer matches the expected source. Refusing to
build without checking whether the terminal inheritance fix is still needed.
This temporary patch tracks #{@upstream_pr}.
"""
end

restore_newlines(patched, newline)
end

defp contains_both?(source, first, second) do
String.contains?(source, first) and String.contains?(source, second)
end

defp newline_style(source) do
if String.contains?(source, "\r\n"), do: :crlf, else: :lf
end

defp normalize_newlines(source), do: String.replace(source, "\r\n", "\n")

defp restore_newlines(source, :crlf), do: String.replace(source, "\n", "\r\n")
defp restore_newlines(source, :lf), do: source
end
49 changes: 49 additions & 0 deletions app/test/linear_cli/release/burrito_patches_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@ defmodule LinearCli.Release.BurritoPatchesTest do

alias LinearCli.Release.BurritoPatches

@unpatched_launcher ~S"""
// On Unix: pipe child stdout through us so we can detect EPIPE from
// the downstream consumer (e.g. `app cmd | head -5`). When the consumer
// exits and breaks the pipe, the copy thread kills the BEAM child.
// On Windows: inherit stdout directly — std.c.read blocks on Windows
// pipes, and the EPIPE group-leader hang is Unix-specific anyway.
var child: std.process.Child = undefined;
var copy_thread: ?std.Thread = null;

if (builtin.os.tag != .windows) {
// The rest of the spawn block is irrelevant to the source patch.
}

const term = if (builtin.os.tag != .windows)
child.wait(io) catch {
copy_thread.?.join();
std.process.exit(0);
}
"""

test "inherits stdout directly when Burrito is connected to a terminal" do
launcher_path = Path.expand("../../../deps/burrito/src/erlang_launcher.zig", __DIR__)
source = File.read!(launcher_path)
Expand All @@ -21,4 +41,33 @@ defmodule LinearCli.Release.BurritoPatchesTest do
BurritoPatches.patch_source!("a different upstream implementation")
end
end

test "matches LF Burrito source when the release hook was checked out with CRLF" do
patch_module_path = Path.expand("../../../release/burrito_patches.exs", __DIR__)

crlf_module_source =
patch_module_path
|> File.read!()
|> String.replace(
"defmodule LinearCli.Release.BurritoPatches do",
"defmodule LinearCli.Release.BurritoPatchesCRLF do"
)
|> String.replace(~r/\r?\n/, "\r\n")

[{crlf_module, _bytecode}] = Code.compile_string(crlf_module_source)
patched = crlf_module.patch_source!(@unpatched_launcher)

assert patched =~ "const stdout_is_tty = Io.File.stdout().isTty(io) catch false;"
assert patched =~ "if (builtin.os.tag != .windows and !stdout_is_tty)"
refute patched =~ "copy_thread.?.join();"
end

test "preserves CRLF when Burrito's source uses CRLF" do
source = String.replace(@unpatched_launcher, "\n", "\r\n")
patched = BurritoPatches.patch_source!(source)

assert patched =~ "\r\n"
refute patched =~ ~r/(?<!\r)\n/
assert BurritoPatches.patch_source!(patched) == patched
end
end