<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Windows on monkshark.dev</title><link>https://monkshark.github.io/tags/windows/</link><description>Recent content in Windows on monkshark.dev</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Sat, 30 May 2026 13:00:00 +0900</lastBuildDate><atom:link href="https://monkshark.github.io/tags/windows/index.xml" rel="self" type="application/rss+xml"/><item><title>#13 - `swift hello.swift` 한 줄이 Windows SDK 링킹까지 닿기까지</title><link>https://monkshark.github.io/p/page-ide-swift-on-windows/</link><pubDate>Sat, 30 May 2026 13:00:00 +0900</pubDate><guid>https://monkshark.github.io/p/page-ide-swift-on-windows/</guid><description>&lt;p&gt;PAGE 의 Run 버튼은 언어를 가리지 않는 자리다. &lt;code&gt;.py&lt;/code&gt; 도, &lt;code&gt;.go&lt;/code&gt; 도, &lt;code&gt;.rs&lt;/code&gt; 도 같은 버튼 하나로 돌아간다. 새 언어를 붙이는 일은 대개 실행 명령 한 줄을 템플릿에 추가하는 정도로 끝난다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;swift&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RunTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;swift&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{file}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Swift 도 그렇게 끝날 줄 알았다. macOS 나 Linux 였다면 그랬을 것이다. 그런데 Windows 에서 이 한 줄은 시작점이었을 뿐, 그 뒤로 한참을 더 가야 했다. 이 글은 &lt;code&gt;swift hello.swift&lt;/code&gt; 한 줄이 Windows SDK 링킹과 tar 파서 교체까지 닿는 동안 지나온 벽들의 기록이다.&lt;/p&gt;
&lt;h2 id="시작--swiftc-를-머신에-올리는-일"&gt;&lt;a href="#%ec%8b%9c%ec%9e%91--swiftc-%eb%a5%bc-%eb%a8%b8%ec%8b%a0%ec%97%90-%ec%98%ac%eb%a6%ac%eb%8a%94-%ec%9d%bc" class="header-anchor"&gt;&lt;/a&gt;시작 — swiftc 를 머신에 올리는 일
&lt;/h2&gt;&lt;p&gt;벽을 이야기하기 전에, 애초에 &lt;code&gt;swiftc&lt;/code&gt; 가 머신에 있어야 한다. Windows 사용자에게 Swift 툴체인 설치를 직접 시킬 수는 없으니, PAGE 는 다른 언어와 같은 방식으로 풀었다. 미리 빌드해 둔 툴체인 번들을 &lt;code&gt;page-ide-assets&lt;/code&gt; 릴리스에서 받아 사용자 디렉터리에 푼다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;assetNamePattern&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;^page-swift-toolchain-&lt;/span&gt;&lt;span class="si"&gt;$osKey&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;$arch&lt;/span&gt;&lt;span class="s2"&gt;-(.+?)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.tar&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.gz$&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;번들은 500MB에서 1.2GB에 이른다. 다른 언어 인스톨러와 같은 모양이라, 받아서 풀고 &lt;code&gt;swiftc&lt;/code&gt;·&lt;code&gt;sourcekit-lsp&lt;/code&gt; 경로를 잡아 두면 이 단계는 끝난다. 적어도 그렇게 보였다. 진짜 벽은 이 &lt;code&gt;swiftc&lt;/code&gt; 를 실제로 굴리려 할 때부터 하나둘 드러났다 — 그리고 그중 하나는 방금 푼 이 번들 안에 숨어 있었다.&lt;/p&gt;
&lt;h2 id="첫-번째-벽--immediate-모드가-없다"&gt;&lt;a href="#%ec%b2%ab-%eb%b2%88%ec%a7%b8-%eb%b2%bd--immediate-%eb%aa%a8%eb%93%9c%ea%b0%80-%ec%97%86%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;첫 번째 벽 — immediate 모드가 없다
&lt;/h2&gt;&lt;p&gt;다른 언어와 똑같이 &lt;code&gt;swift {file}&lt;/code&gt; 를 실행했더니, Windows 에서는 즉시 죽었다. macOS·Linux 의 &lt;code&gt;swift foo.swift&lt;/code&gt; 는 파일을 그 자리에서 해석·실행하는 immediate 모드를 쓴다. 이 모드는 LLVM 의 ORC JIT 위에서 동작하는데, Windows 용 Swift 툴체인은 이 JIT 실행을 지원하지 않는다. 인터프리터처럼 한 번에 돌릴 방법이 없는 것이다.&lt;/p&gt;
&lt;p&gt;그래서 Swift 만은 다른 길로 보냈다. 한 줄 실행 대신 &amp;ldquo;먼저 컴파일하고, 나온 exe 를 실행한다&amp;rdquo; 는 2단계 모델이다. PAGE 의 Run 설정에는 본 실행 전에 돌릴 사전 명령(prelaunch)을 넣을 자리가 있어서, 여기에 &lt;code&gt;swiftc&lt;/code&gt; 컴파일을 끼워 넣었다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;swiftWindowsPrelaunch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;swiftc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-use-ld=lld&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-o&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;.exe&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Run 을 누르면 &lt;code&gt;swiftc&lt;/code&gt; 가 먼저 돌아 &lt;code&gt;hello.exe&lt;/code&gt; 를 만들고, 그다음 그 exe 를 실행한다. 화면상으로는 여전히 버튼 하나지만, 안에서는 컴파일과 실행이 나뉘어 돌아간다. 여기까지는 순조로웠다. &lt;code&gt;print(&amp;quot;hello&amp;quot;)&lt;/code&gt; 한 줄짜리는 잘 돌았다.&lt;/p&gt;
&lt;h2 id="두-번째-벽--import-foundation-이-빌드되지-않는다"&gt;&lt;a href="#%eb%91%90-%eb%b2%88%ec%a7%b8-%eb%b2%bd--import-foundation-%ec%9d%b4-%eb%b9%8c%eb%93%9c%eb%90%98%ec%a7%80-%ec%95%8a%eb%8a%94%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;두 번째 벽 — &lt;code&gt;import Foundation&lt;/code&gt; 이 빌드되지 않는다
&lt;/h2&gt;&lt;p&gt;문제는 그다음이었다. &lt;code&gt;print&lt;/code&gt; 만 쓰는 코드는 드물다. 날짜를 다루든 문자열을 포매팅하든, 조금만 실용적인 코드가 되면 &lt;code&gt;import Foundation&lt;/code&gt; 이 등장한다. 그리고 Windows 에서 이 한 줄을 넣은 순간 &lt;code&gt;swiftc&lt;/code&gt; 가 헤더를 찾지 못해 무너졌다.&lt;/p&gt;
&lt;p&gt;원인을 따라가 보니 Foundation 은 Windows 에서 C 런타임과 Windows SDK 위에 얹혀 있었다. 즉 Swift 코드를 컴파일하려는데 MSVC 의 C 헤더와 Windows SDK 헤더·라이브러리가 있어야 했다. Swift 툴체인만 깔아서는 절반만 갖춘 셈이었다. 평범한 Windows 개발자라면 Visual Studio 를 통째로 설치해 해결하겠지만, PAGE 는 IDE 안에서 필요한 것만 내려받아 설치하는 흐름을 지향한다. 수 GB짜리 Visual Studio 설치를 사용자에게 강요할 수는 없었다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;xwin&lt;/code&gt; 을 찾았다. Microsoft 의 재배포 가능 패키지에서 MSVC CRT 와 Windows SDK 의 헤더·라이브러리만 골라 받아오는 도구다. 이걸 감싸는 &lt;code&gt;WindowsSdkInstaller&lt;/code&gt; 를 만들어, xwin 으로 받은 산출물(splat)에서 &lt;code&gt;INCLUDE&lt;/code&gt;·&lt;code&gt;LIB&lt;/code&gt; 경로를 구성하고 컴파일 환경에 주입하도록 했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;buildEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;splat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;INCLUDE&amp;#34;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;includeDirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;splat&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;joinToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pathSeparator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;LIB&amp;#34;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;libDirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;splat&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;joinToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pathSeparator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;File.pathSeparator&lt;/code&gt; 를 쓴 건 나중에 CI 에서 톡톡히 값을 했는데, 그 이야기는 뒤에서 다시 한다.&lt;/p&gt;
&lt;h2 id="세-번째-벽--사라진-corecrt_mathh"&gt;&lt;a href="#%ec%84%b8-%eb%b2%88%ec%a7%b8-%eb%b2%bd--%ec%82%ac%eb%9d%bc%ec%a7%84-corecrt_mathh" class="header-anchor"&gt;&lt;/a&gt;세 번째 벽 — 사라진 &lt;code&gt;corecrt_math.h&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;헤더 경로를 다 맞췄는데도 컴파일이 깨졌다. 이번엔 메시지가 구체적이었다. &lt;code&gt;corecrt_math.h&lt;/code&gt; 를 찾을 수 없다는 것이다.&lt;/p&gt;
&lt;p&gt;xwin 이 받아온 SDK 버전은 최신(26100)이었는데, 이 버전에서 &lt;code&gt;corecrt_math.h&lt;/code&gt; 헤더가 제거되어 있었다. 그런데 같은 SDK 안의 ucrt modulemap 은 여전히 이 헤더를 참조하는 모듈을 선언하고 있었다. 헤더는 없는데 모듈 정의는 그 헤더를 가리키니, Clang 이 모듈을 구성하려다 없는 파일에 걸려 넘어진 것이다.&lt;/p&gt;
&lt;p&gt;해결은 외과적으로 갔다. modulemap 에서 없는 헤더를 가리키는 그 서브모듈만 도려내는 패치를 만들었다. 헤더가 실제로 있으면 건드리지 않고, 없을 때만 해당 선언을 제거한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;patchMissingCorecrtMath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modulemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;modulemap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MATH_SUBMODULE_REGEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;이걸 splat 배포 단계(&lt;code&gt;deployModulemaps&lt;/code&gt;)에 끼워, 헤더 누락 여부를 보고 조건부로 적용했다. 헤더가 있는 구버전 SDK 에서는 원본을 그대로 두고, 없는 신버전에서만 패치가 작동한다. 테스트도 두 경우를 모두 고정해 두었다 — 누락 시 도려내고, 존재 시 보존한다.&lt;/p&gt;
&lt;h2 id="네-번째-벽--링크는-또-별개였다"&gt;&lt;a href="#%eb%84%a4-%eb%b2%88%ec%a7%b8-%eb%b2%bd--%eb%a7%81%ed%81%ac%eb%8a%94-%eb%98%90-%eb%b3%84%ea%b0%9c%ec%98%80%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;네 번째 벽 — 링크는 또 별개였다
&lt;/h2&gt;&lt;p&gt;컴파일이 통과하자 이번엔 링크에서 막혔다. Foundation 의 심볼을 찾지 못한다는 것이다. 헤더로 컴파일이 되는 것과, 실제 Foundation 구현을 실행 파일에 이어 붙이는 것은 별개의 일이었다. Foundation 의 import 라이브러리(&lt;code&gt;Foundation.lib&lt;/code&gt;)를 링커에 명시적으로 넘겨줘야 했다.&lt;/p&gt;
&lt;p&gt;prelaunch 명령에 링커 인자를 더 얹었다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;swiftc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-use-ld=lld&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-Xcc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-Xclang&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-Xcc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-fbuiltin-headers-in-system-modules&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-Xlinker&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;foundationLib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-o&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;.exe&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;-fbuiltin-headers-in-system-modules&lt;/code&gt; 는 Clang 의 빌트인 헤더가 시스템 모듈과 충돌하는 걸 막는 플래그였고, &lt;code&gt;-Xlinker&lt;/code&gt; 로 Foundation import 라이브러리를 직접 물렸다. 이 조합을 맞추고서야 &lt;code&gt;import Foundation&lt;/code&gt; 을 쓴 코드가 빌드·링크·실행까지 한 번에 흘렀다.&lt;/p&gt;
&lt;h2 id="다섯-번째-벽--tar-가-긴-경로를-잘라먹었다"&gt;&lt;a href="#%eb%8b%a4%ec%84%af-%eb%b2%88%ec%a7%b8-%eb%b2%bd--tar-%ea%b0%80-%ea%b8%b4-%ea%b2%bd%eb%a1%9c%eb%a5%bc-%ec%9e%98%eb%9d%bc%eb%a8%b9%ec%97%88%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;다섯 번째 벽 — tar 가 긴 경로를 잘라먹었다
&lt;/h2&gt;&lt;p&gt;컴파일·링크가 풀리고 나서야, 앞서 넘어왔다고 생각한 설치 단계에 사실은 금이 가 있었다는 걸 알았다. 맨 앞에서 받아 푼 그 툴체인 번들 말이다. 추출은 끝났는데도 일부 &lt;code&gt;.swiftmodule&lt;/code&gt; 을 import 할 때 모듈을 찾지 못했다. 경로가 미묘하게 잘려 있었다.&lt;/p&gt;
&lt;p&gt;PAGE 의 옛 tar 추출기는 직접 구현한 ustar 리더였다. ustar 포맷은 파일 이름을 100바이트 필드에 담는데, Swift 툴체인의 모듈 경로는 이 한계를 넘는 것들이 있었다. GNU tar 는 이런 긴 이름을 &lt;code&gt;@LongLink&lt;/code&gt;(typeflag &amp;lsquo;L&amp;rsquo;) 라는 별도 엔트리로, 혹은 PAX 확장 헤더로 따로 기록한다. 우리 리더는 이 확장 엔트리를 해석하지 못하고 100바이트에서 그냥 잘랐다. 그러니 긴 경로의 모듈이 엉뚱한 이름으로 풀려 import 가 실패한 것이다.&lt;/p&gt;
&lt;p&gt;직접 짠 리더로 tar 의 온갖 확장 포맷을 다 떠받치는 건 무리였다. 옛 리더(TarReader)를 들어내고 Apache Commons Compress 의 &lt;code&gt;TarArchiveInputStream&lt;/code&gt; 으로 추출 경로를 통일했다. &lt;code&gt;@LongLink&lt;/code&gt; 도 PAX 헤더도 알아서 처리하는 검증된 구현이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;TarArchiveInputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tar&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="py"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nextEntry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ... 긴 경로도 entry.name 에 온전히 담겨 온다
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nextEntry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;리더를 바꾸자 잘림이 사라졌고, 모듈 경로가 온전히 복원됐다.&lt;/p&gt;
&lt;h2 id="여섯-번째-벽--duplicate-values-for-key-path"&gt;&lt;a href="#%ec%97%ac%ec%84%af-%eb%b2%88%ec%a7%b8-%eb%b2%bd--duplicate-values-for-key-path" class="header-anchor"&gt;&lt;/a&gt;여섯 번째 벽 — Duplicate values for key &amp;lsquo;Path&amp;rsquo;
&lt;/h2&gt;&lt;p&gt;이제 진짜 다 됐다고 생각했을 때, 실행 단계에서 낯선 크래시를 만났다. Swift 런타임이 &lt;code&gt;Fatal error: Duplicate values for key 'Path'&lt;/code&gt; 를 뱉으며 죽었다.&lt;/p&gt;
&lt;p&gt;Swift 의 &lt;code&gt;ProcessInfo.environment&lt;/code&gt; 는 키를 대소문자 구분 없이(case-insensitive) 다룬다. 그런데 Windows 환경 변수에는 &lt;code&gt;Path&lt;/code&gt; 와 &lt;code&gt;PATH&lt;/code&gt; 가 둘 다 있을 수 있다. Swift 입장에서는 같은 키가 둘이니, 딕셔너리를 만들다 중복으로 터진 것이다.&lt;/p&gt;
&lt;p&gt;처음엔 PAGE 가 자식 프로세스에 환경을 넘기면서 &lt;code&gt;Path&lt;/code&gt; 를 중복으로 집어넣은 줄 알았다. 그래서 재현을 시도했는데, 아무리 해도 Java 경로에서는 중복이 재현되지 않았다. 파고들어 보니 Java 의 &lt;code&gt;ProcessBuilder&lt;/code&gt; 는 자식을 띄우는 순간 환경 블록을 만들면서 대소문자가 같은 키를 이미 한 번 정리해서 넘긴다. 즉 Java 가 띄운 자식은 애초에 중복 &lt;code&gt;Path&lt;/code&gt;/&lt;code&gt;PATH&lt;/code&gt; 를 볼 수 없었다. 그 크래시는 깨진 설치 상태에서 비정상적으로 흘러든 환경이었던 것으로 결론지었다 — PAGE 코드가 만든 버그가 아니었다.&lt;/p&gt;
&lt;p&gt;그래도 같은 모양의 사고가 다시 나는 건 막고 싶었다. 자식 프로세스 환경을 넘기기 직전에 대소문자가 겹치는 키를 한 번 정리하는 방어 코드를 넣었다. 버그를 고친다기보다, 환경 변수 충돌의 여지 자체를 닫아두는 쪽이다.&lt;/p&gt;
&lt;h2 id="일곱-번째-벽이라기보단-기다림"&gt;&lt;a href="#%ec%9d%bc%ea%b3%b1-%eb%b2%88%ec%a7%b8-%eb%b2%bd%ec%9d%b4%eb%9d%bc%ea%b8%b0%eb%b3%b4%eb%8b%a8-%ea%b8%b0%eb%8b%a4%eb%a6%bc" class="header-anchor"&gt;&lt;/a&gt;일곱 번째 벽이라기보단, 기다림
&lt;/h2&gt;&lt;p&gt;마지막은 벽이 아니라 체감 속도였다. &lt;code&gt;import Foundation&lt;/code&gt; 한 줄을 처음 컴파일하면 Clang 이 모듈 캐시를 통째로 구워야 한다. 이 캐시가 100MB를 훌쩍 넘는다. 측정해 보니 첫 실행이 약 10초였다. Run 한 번에 10초를 기다리는 건 인터프리터 언어의 즉시성에 익숙한 손에는 한참이었다.&lt;/p&gt;
&lt;p&gt;다만 이 비용은 한 번뿐이다. 모듈 캐시가 자리를 잡고 나면 재실행은 약 2.5초로 떨어졌다. 그래서 소스가 바뀌지 않았다면 굳이 다시 컴파일할 이유가 없었다. make 가 하는 일을 작게 흉내 낸 &lt;code&gt;BuildCache&lt;/code&gt; 를 두어, 출력물이 모든 입력 소스보다 새것이고 빌드 명령이 그대로면 컴파일 단계를 통째로 건너뛰게 했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;upToDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;buildKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;marker&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolveSibling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${output.fileName}&lt;/span&gt;&lt;span class="s2"&gt;.pagebuild&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;buildKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;outTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLastModifiedTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;toMillis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getLastModifiedTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;it&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;toMillis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;outTime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;빌드 명령 동일성까지 키에 넣은 건, 컴파일 플래그가 바뀌었는데 산출물 시각만 보고 &amp;ldquo;최신&amp;rdquo; 이라 착각하는 걸 막기 위해서다. 소스도 플래그도 그대로일 때만 캐시가 적중한다.&lt;/p&gt;
&lt;h2 id="ci-가-잡아준-마지막-한-줄"&gt;&lt;a href="#ci-%ea%b0%80-%ec%9e%a1%ec%95%84%ec%a4%80-%eb%a7%88%ec%a7%80%eb%a7%89-%ed%95%9c-%ec%a4%84" class="header-anchor"&gt;&lt;/a&gt;CI 가 잡아준 마지막 한 줄
&lt;/h2&gt;&lt;p&gt;여담 하나. &lt;code&gt;buildEnv&lt;/code&gt; 가 &lt;code&gt;INCLUDE&lt;/code&gt;·&lt;code&gt;LIB&lt;/code&gt; 를 이을 때 &lt;code&gt;File.pathSeparator&lt;/code&gt; 를 쓴다고 앞에서 적었다. 이걸 검증하는 테스트를 짜면서, 처음엔 입력 경로를 &lt;code&gt;Path(&amp;quot;C:&amp;quot;, &amp;quot;splat&amp;quot;)&lt;/code&gt; 같은 Windows 스타일로 박아 넣었다. 로컬(Windows)에서는 멀쩡히 통과했다.&lt;/p&gt;
&lt;p&gt;그런데 CI 는 ubuntu 에서 돈다. Linux 에서 경로 구분자는 &lt;code&gt;:&lt;/code&gt; 다. &lt;code&gt;C:&lt;/code&gt; 의 콜론이 경로 문자열에 그대로 남아 &lt;code&gt;:&lt;/code&gt; 로 split 하는 검증과 충돌하면서 테스트가 깨졌다. 코드가 아니라 테스트가 OS 에 의존하고 있었던 것이다. &lt;code&gt;Files.createTempDirectory&lt;/code&gt; 로 OS 가 알아서 만들어 주는 경로를 쓰도록 고쳐, 어느 플랫폼에서 돌든 같은 결과가 나오게 했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;splat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createTempDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;page-splat-env&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;env&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;buildEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;splat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;sep&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pathSeparator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;includeDirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;splat&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;INCLUDE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Windows 를 지원하려고 시작한 작업의 테스트가 Linux 에서 깨진 건, 돌이켜보면 이 글 전체를 요약하는 장면 같기도 하다. 한 플랫폼의 당연함이 다른 플랫폼에서는 당연하지 않다.&lt;/p&gt;
&lt;h2 id="돌아보며"&gt;&lt;a href="#%eb%8f%8c%ec%95%84%eb%b3%b4%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;돌아보며
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;swift {file}&lt;/code&gt; 한 줄로 끝날 줄 알았던 작업은, immediate 모드의 부재로 시작해 swiftc 2단계 모델, MSVC·Windows SDK 확보, 사라진 헤더의 modulemap 패치, Foundation 링크, tar long-name, 환경 변수 중복, 그리고 증분 캐시까지 일곱 겹의 벽을 지나서야 다른 언어와 같은 자리에 섰다.&lt;/p&gt;
&lt;p&gt;벽 하나하나는 사실 Windows 와 Swift 가 서로를 전제하지 않아 생긴 틈이었다. macOS 라면 OS 가 메워 줬을 틈을, Windows 에서는 IDE 가 직접 메워야 했다. PAGE 가 &amp;ldquo;필요한 걸 IDE 안에서 받아 설치한다&amp;rdquo; 는 방향을 고집하는 한, 이런 틈을 메우는 일은 계속 나올 것이다. 다음 언어에서는 또 어떤 당연함이 당연하지 않을지, 그건 그때 가서 또 부딪혀 보기로 한다.&lt;/p&gt;</description></item><item><title>#12 - `gem install solargraph` 한 줄에서 prebuilt 번들 다중 버전까지</title><link>https://monkshark.github.io/p/page-ide-ruby-bootstrap-rabbit-hole/</link><pubDate>Sat, 23 May 2026 22:00:00 +0900</pubDate><guid>https://monkshark.github.io/p/page-ide-ruby-bootstrap-rabbit-hole/</guid><description>&lt;p&gt;처음 잡았을 때 한 줄로 적었다. &lt;code&gt;gem install solargraph&lt;/code&gt;. PAGE 의 LSP 자동 설치를 모든 언어에 펴는 작업에서 Ruby 자리에 들어갈 명령이었다. 다른 언어들 — Perl 의 &lt;code&gt;cpan&lt;/code&gt;, R 의 &lt;code&gt;Rscript&lt;/code&gt;, OCaml 의 &lt;code&gt;opam&lt;/code&gt; — 도 한 줄짜리 매니저 호출로 끝났다. Ruby 도 같은 줄에 들어갈 거라고 봤다.&lt;/p&gt;
&lt;p&gt;그 한 줄이 매니저 한 호출이 아니라 prebuilt 번들 다운로드와 안티바이러스 감지와 다중 버전 노출까지 늘어지는 동안, 같은 자리에 다른 운영체제가 다른 가정을 깔고 있었다는 점을 그때마다 한 번씩 다시 배웠다.&lt;/p&gt;
&lt;h2 id="한-자리-세-가정"&gt;&lt;a href="#%ed%95%9c-%ec%9e%90%eb%a6%ac-%ec%84%b8-%ea%b0%80%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;한 자리, 세 가정
&lt;/h2&gt;&lt;p&gt;PAGE 의 LSP 자동 설치는 매니저별로 한 어댑터를 두는 구조다. GitHub Releases 갈래는 &lt;code&gt;GitHubReleaseInstaller&lt;/code&gt;, npm 갈래는 &lt;code&gt;NpmGlobalInstaller&lt;/code&gt;, 시스템 패키지 매니저 갈래는 &lt;code&gt;ShellPackageInstaller&lt;/code&gt;. Ruby 는 — 첫 가정상 — &lt;code&gt;ShellPackageInstaller&lt;/code&gt; 한 자리에 들어가면 됐다. &lt;code&gt;gem install --no-document solargraph&lt;/code&gt; 한 줄.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;shellRubyInstaller&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;LspInstaller&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ShellPackageInstaller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ShellPackageDescriptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;languageId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ruby&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;displayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;solargraph&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;managerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;gem&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;managerInstallUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://www.ruby-lang.org/en/downloads/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;binaryName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;solargraph&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;packageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;solargraph&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;buildInstallCommand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;install&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--no-document&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;리눅스와 맥은 시스템에 ruby 가 깔려 있다는 가정 — &lt;code&gt;/usr/bin/ruby&lt;/code&gt; 든 Homebrew 의 &lt;code&gt;ruby&lt;/code&gt; 든 — 위에서 같은 자리에 들어갈 거라고 봤다. &lt;code&gt;gem&lt;/code&gt; 한 명령에 solargraph 한 줄. 같은 자리의 다른 매니저들 (&lt;code&gt;cpan&lt;/code&gt;, &lt;code&gt;opam&lt;/code&gt;, &lt;code&gt;Rscript&lt;/code&gt;) 과 같은 모양. 실제 그 두 OS 에서 한 번씩 돌려 검증한 자리는 — 손에 그 자리가 없어서 — 비어 있다. 다만 코드 자리에 그 모양으로 어댑터를 끼워 둔 상태였고, 다음 자리에서 들춰 볼 수 있게 했다. Windows 만 시작부터 자리가 비어 있었다.&lt;/p&gt;
&lt;h2 id="windows-의-첫-벽--ruby-가-없다"&gt;&lt;a href="#windows-%ec%9d%98-%ec%b2%ab-%eb%b2%bd--ruby-%ea%b0%80-%ec%97%86%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;Windows 의 첫 벽 — ruby 가 없다
&lt;/h2&gt;&lt;p&gt;Windows 는 ruby 자체가 시스템에 없다. macOS 가 &lt;code&gt;/usr/bin/ruby&lt;/code&gt; 를 가진 것과도, 리눅스 배포판 대부분이 &lt;code&gt;apt&lt;/code&gt;/&lt;code&gt;dnf&lt;/code&gt; 한 번으로 ruby 를 받을 수 있는 것과도 다르다. RubyInstaller2 라는 사실상의 표준 배포가 있긴 하지만 그것 자체가 — 설치 마법사 한 번을 사용자가 끝내야 하고, MSYS2 와 MinGW devkit 의 추가 설치 단계가 있고, 그 모든 게 PATH 변경과 환경변수 설정을 동반한다.&lt;/p&gt;
&lt;p&gt;PAGE 의 자동 설치 약속은 한 클릭으로 설치 다이얼로그를 끝내는 것이었다. 사용자가 따로 RubyInstaller 를 받아 마법사를 돌리고 PATH 를 설정하라는 한 줄을 띄우는 자리에서는 이 약속이 깨진다.&lt;/p&gt;
&lt;p&gt;첫 시도는 RubyInstaller2 의 silent installer + MSYS2 부트스트랩이었다. 다운로드 → &lt;code&gt;/silent&lt;/code&gt; 플래그로 실행 → 끝난 자리에서 &lt;code&gt;ridk install&lt;/code&gt; 로 devkit 까지. 코드는 짧았다. 실행은 길었다.&lt;/p&gt;
&lt;h2 id="uac-와-defender-가-멈춘-자리"&gt;&lt;a href="#uac-%ec%99%80-defender-%ea%b0%80-%eb%a9%88%ec%b6%98-%ec%9e%90%eb%a6%ac" class="header-anchor"&gt;&lt;/a&gt;UAC 와 Defender 가 멈춘 자리
&lt;/h2&gt;&lt;p&gt;Silent installer 가 UAC 동의창을 띄웠다. 한 사용자가 &amp;ldquo;예&amp;rdquo; 를 누르지 않으면 다음 한 줄로 못 갔다. &amp;ldquo;예&amp;rdquo; 를 누른 뒤에도, MSYS2 의 패키지 매니저 &lt;code&gt;pacman&lt;/code&gt; 이 첫 동기화에서 — Windows Defender 의 행위 기반 차단에 — 자식 프로세스 fork 가 막혔다. 어떤 사용자의 환경에서는 됐고, 어떤 환경에서는 30 분 동안 멈춰 있었다.&lt;/p&gt;
&lt;p&gt;원인의 한 줄은 단순했다. MSYS2 의 fork 에뮬레이션은 Windows 의 ASR (Attack Surface Reduction) 규칙 한 갈래에 닿는다. 사용자가 그 규칙을 만지지 않은 상태에서는 — 그게 기본값이다 — 부트스트랩이 한 줄에서 멈춘다. 사람 손이 닿아야 하는 자리가 다시 한 자리 생겼다.&lt;/p&gt;
&lt;p&gt;이 시점에서 한 결정이 필요했다 — Windows 에서의 부트스트랩을 계속 사용자 컴퓨터 안에서 돌릴 것인가, 아니면 결과물만 배달할 것인가.&lt;/p&gt;
&lt;h2 id="prebuilt-번들로-자리-옮기기"&gt;&lt;a href="#prebuilt-%eb%b2%88%eb%93%a4%eb%a1%9c-%ec%9e%90%eb%a6%ac-%ec%98%ae%ea%b8%b0%ea%b8%b0" class="header-anchor"&gt;&lt;/a&gt;prebuilt 번들로 자리 옮기기
&lt;/h2&gt;&lt;p&gt;두 번째 시도는 자리 자체를 옮기는 결정이었다. 부트스트랩의 모든 단계 — Ruby + MSYS2 + MinGW UCRT64 + solargraph + 의존 gem 들 — 을 한 zip 으로 만들어 둔 자리에서 받아 가게.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;DEFAULT&lt;/span&gt;&lt;span class="n"&gt;_RUBY_VERSION&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.4.6&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;DEFAULT&lt;/span&gt;&lt;span class="n"&gt;_SOLARGRAPH_VERSION&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0.55.4&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;DEFAULT&lt;/span&gt;&lt;span class="n"&gt;_RUBY_BUNDLE_RELEASE&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ruby-bundle&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;DEFAULT&lt;/span&gt;&lt;span class="n"&gt;_RUBY_BUNDLE_REPO&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;monkshark/page-ide-assets&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;WINDOWS&lt;/span&gt;&lt;span class="n"&gt;_BUNDLE_NAME&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;^page-ruby-solargraph-windows-x86_64-(.+?)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.zip$&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;monkshark/page-ide-assets&lt;/code&gt; 라는 별도 GitHub 레포에 GitHub Actions 워크플로 한 잡을 두고, 그 잡이 Windows runner 한 자리에서 RubyInstaller + MSYS2 + solargraph 까지 다 굴린 다음 결과 디렉토리를 zip 한 파일로 압축해서 release asset 에 올린다. PAGE 의 Windows 클라이언트는 그 zip 하나만 받아서 풀면 끝. 사용자 컴퓨터에서는 fork 가 없고 ASR 가 끼어들 자리가 없고 UAC 가 뜰 일이 없다.&lt;/p&gt;
&lt;p&gt;설치 단계는 한 줄로 줄었다. 다운로드 + 압축 해제. 부트스트랩이 아니라 배달.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;installFromPrebuiltBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LspInstaller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rubyRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createDirectories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;detectThirdPartyAntivirus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;requestDefenderExclusion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;bundle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obtainBundleZip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;zipExtractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;solargraph&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;solargraphBinary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;solargraph&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;solargraph.bat missing after bundle extraction: &lt;/span&gt;&lt;span class="si"&gt;$solargraph&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="다섯-자리의-graceful"&gt;&lt;a href="#%eb%8b%a4%ec%84%af-%ec%9e%90%eb%a6%ac%ec%9d%98-graceful" class="header-anchor"&gt;&lt;/a&gt;다섯 자리의 graceful
&lt;/h2&gt;&lt;p&gt;자리 자체는 옮겼지만, 같은 자리에서 막힌 사람들이 옮긴 뒤에도 막혔다. 사용자 환경에 따라 다섯 가지 자리가 더 필요했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3rd-party AV 감지&lt;/strong&gt;. Defender 가 아니라 노턴/카스퍼스키/맥아피 같은 다른 안티바이러스가 깔린 자리. 그쪽도 zip 추출 후의 &lt;code&gt;.exe&lt;/code&gt; / &lt;code&gt;.dll&lt;/code&gt; 를 검역하는 경우가 있다. 압축 해제 전에 한 번 감지해 두고 사용자에게 미리 안내한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;detectThirdPartyAntivirus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Defender 자동 exclusion&lt;/strong&gt;. 정책 허용 한도 안에서 설치 디렉토리를 Defender 검사 대상에서 빼는 PowerShell 한 줄을 시도한다. 사용자가 관리자 권한을 가진 자리에서는 통과하고, 아닌 자리에서는 — 거부됐다는 신호를 받고 — 그 자리에 매뉴얼 안내를 띄운다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UAC 거부 graceful&lt;/strong&gt;. exclusion 요청이 UAC 에서 거부된 자리에서 설치를 멈추지 않는다. AV 검역이 일어날 수 있다는 한 줄의 경고와 함께 그대로 진행한다. 사용자가 손해를 보는 자리가 더 적은 쪽을 택했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;환경변수 fallback&lt;/strong&gt;. 사내망이나 에어갭 환경에서 GitHub Releases 에 못 닿는 자리. &lt;code&gt;PAGE_RUBY_BUNDLE_OVERRIDE&lt;/code&gt; 환경변수 한 자리에 로컬 zip 경로를 박을 수 있게 했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;bundleOverridePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;PAGE_RUBY_BUNDLE_OVERRIDE&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;strong&gt;실 컴파일 검증&lt;/strong&gt;. assets 레포 워크플로가 빌드한 번들이 실제로 설치 가능한지 — 그리고 solargraph 가 정말 시동되는지 — 같은 워크플로 끝에서 다른 runner 한 자리를 띄워 검증했다. 빌드 잡과 검증 잡을 분리해서, 검증이 실패하면 release 자체가 발행되지 않게.&lt;/p&gt;
&lt;h2 id="다중-버전--한-자리에-여러-답"&gt;&lt;a href="#%eb%8b%a4%ec%a4%91-%eb%b2%84%ec%a0%84--%ed%95%9c-%ec%9e%90%eb%a6%ac%ec%97%90-%ec%97%ac%eb%9f%ac-%eb%8b%b5" class="header-anchor"&gt;&lt;/a&gt;다중 버전 — 한 자리에 여러 답
&lt;/h2&gt;&lt;p&gt;처음에는 한 버전만 받았다. &lt;code&gt;DEFAULT_RUBY_VERSION = &amp;quot;3.4.6&amp;quot;&lt;/code&gt;. 그 자리에 한 자리만 있었다. Ruby 3.3 을 쓰는 프로젝트가 있을 수 있고 3.5 의 새 기능을 쓰는 자리가 있을 수 있는데, IDE 의 설치 다이얼로그에는 단일 버전만 떠 있었다.&lt;/p&gt;
&lt;p&gt;다중 버전은 — 빌드 시점에 한 자리 더 만들고, IDE 에서 그 자리들을 동적으로 노출하는 — 두 단계로 갈라졌다.&lt;/p&gt;
&lt;p&gt;빌드 시점은 assets 레포 워크플로의 매트릭스에 ruby 버전 한 축을 더하는 것. &lt;code&gt;3.3.x&lt;/code&gt; / &lt;code&gt;3.4.x&lt;/code&gt; / &lt;code&gt;3.5.x&lt;/code&gt; 셋이 같은 release 에 다른 자산 이름으로 올라간다 — &lt;code&gt;page-ruby-solargraph-windows-x86_64-3.3.6.zip&lt;/code&gt;, &lt;code&gt;...-3.4.6.zip&lt;/code&gt;, &lt;code&gt;...-3.5.1.zip&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;런타임 노출은 IDE 가 GitHub Releases API 로 그 release 의 asset 목록을 받아서, 자산 이름 정규식으로 버전을 추출하는 것.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-kotlin" data-lang="kotlin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;availableVersions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="py"&gt;discovered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;osKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;windows&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;discoverWindowsBundleVersions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;macos&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;discoverMacPortableVersions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discovered&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;defaultRubyVersion&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;distinct&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;sortedWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VERSION_DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;discoverWindowsBundleVersions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="py"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rubyBundleRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;runCatching&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;versionsFetcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rubyBundleRelease&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mapNotNull&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;WINDOWS_BUNDLE_NAME&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;groupValues&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;CLEAN_VERSION_REGEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="n"&gt;getOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;assets 레포에 ruby 3.6 zip 한 자리가 새로 올라가면, IDE 의 설치 다이얼로그는 다음 열렸을 때 그 한 줄을 더 보여준다. 코드 변경 없이.&lt;/p&gt;
&lt;p&gt;macOS 자리는 — Homebrew 의 &lt;code&gt;homebrew-portable-ruby&lt;/code&gt; 라는 별도 자리에서 — 비슷한 방식으로 받았다. 한쪽이 자체 빌드 매트릭스라면 한쪽은 외부 portable 자산 목록이라는 차이가 있을 뿐, 한 자리에서 여러 버전을 동적으로 채운다는 결정은 같다.&lt;/p&gt;
&lt;h2 id="돌아보면"&gt;&lt;a href="#%eb%8f%8c%ec%95%84%eb%b3%b4%eb%a9%b4" class="header-anchor"&gt;&lt;/a&gt;돌아보면
&lt;/h2&gt;&lt;p&gt;한 줄로 끝낼 수 있다고 봤던 자리가 여러 단계로 갈라졌다. 그 한 줄이 늘어진 자리들은 — 다시 적으면 — 모두 같은 한 질문의 자리였다.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;사용자의 컴퓨터 안에서 무엇을 어디까지 돌릴 것인가.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;리눅스/맥에서는 그 답이 한 줄짜리 매니저 호출. Windows 에서는 — 부트스트랩 단계가 ASR 와 UAC 와 Defender 사이로 흘러 들어가는 자리에서는 — 그 답이 prebuilt 번들 한 자리에서 받아 가기. 사용자 컴퓨터에서 안 돌릴 수 있는 자리는 안 돌리는 쪽이 — 사람 손이 닿을 자리를 줄이는 쪽이 — 결국 약속을 지키는 자리였다.&lt;/p&gt;
&lt;p&gt;다른 한 가지는 한 자리에 답을 하나만 두지 않는다는 점. 다운로드 한 자리에 GitHub Releases 가 답하고, 그 자리가 막힌 사용자에게는 &lt;code&gt;PAGE_RUBY_BUNDLE_OVERRIDE&lt;/code&gt; 환경변수가 답하고, 그 자리에서도 막힌 경우에는 매뉴얼 가이드 한 줄이 답한다. AV 검역의 자리에 Defender exclusion 시도가 답하고, UAC 거부의 자리에 graceful 한 줄이 답한다. 같은 자리에 답이 여러 개 있으면, 그 자리의 어느 한 답이 막혀도 다음 답이 자기 자리를 받는다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gem install solargraph&lt;/code&gt; 한 줄로 끝났을 자리가 여러 자리로 갈라졌는데, 그 자리들을 다시 한 줄에 응축하면 — 한 명령의 약속을 한 클릭이 지키게 만드는 자리들 — 결국 같은 한 결정이 반복된 자리였다. 다음 언어 자리 — 다음 운영체제 자리 — 가 같은 식으로 늘어질 때, 이번 자리에서 깔아 둔 어댑터 계층이 그 자리들을 한 줄로 다시 줄여 줄 거라고 본다.&lt;/p&gt;</description></item></channel></rss>