@@ -38,7 +38,8 @@ def executable_candidate_paths = test_executable_candidate_paths
3838 allow ( File ) . to receive ( :stat ) . with ( setuid_bubblewrap ) . and_return ( instance_double ( File ::Stat , setuid? : true ) )
3939 end
4040
41- it "skips setuid bubblewrap candidates" do
41+ it "searches Homebrew Bubblewrap before system Bubblewrap and skips setuid candidates" do
42+ expect ( klass . executable_candidate_paths . to_a ) . to start_with ( "#{ HOMEBREW_PREFIX } /bin" , "/usr/bin" , "/bin" )
4243 expect ( sandbox_class . bubblewrap_executable ) . to eq ( usable_bubblewrap )
4344 end
4445
@@ -62,6 +63,8 @@ def executable_candidate_paths = test_executable_candidate_paths
6263 end
6364 let ( :bubblewrap_dir ) { mktmpdir }
6465 let ( :bubblewrap ) { bubblewrap_dir /"bwrap" }
66+ let ( :fallback_bubblewrap_dir ) { mktmpdir }
67+ let ( :fallback_bubblewrap ) { fallback_bubblewrap_dir /"bwrap" }
6568
6669 before do
6770 FileUtils . touch bubblewrap
@@ -103,6 +106,43 @@ def executable_candidate_paths = test_executable_candidate_paths
103106 expect ( sandbox_class . failure_reason ) . to be_nil
104107 end
105108
109+ it "probes later usable Bubblewrap candidates if earlier candidates fail" do
110+ FileUtils . touch fallback_bubblewrap
111+ FileUtils . chmod "+x" , fallback_bubblewrap
112+ sandbox_class . test_executable_candidate_paths = PATH . new ( bubblewrap_dir , fallback_bubblewrap_dir )
113+
114+ expect ( sandbox_class ) . to receive ( :system ) . with (
115+ bubblewrap . to_s ,
116+ "--unshare-user" ,
117+ "--unshare-ipc" ,
118+ "--unshare-pid" ,
119+ "--unshare-uts" ,
120+ "--unshare-cgroup-try" ,
121+ "--ro-bind" , "/" , "/" ,
122+ "--proc" , "/proc" ,
123+ "--dev" , "/dev" ,
124+ "true" ,
125+ out : File ::NULL ,
126+ err : File ::NULL
127+ ) . and_return ( false )
128+ expect ( sandbox_class ) . to receive ( :system ) . with (
129+ fallback_bubblewrap . to_s ,
130+ "--unshare-user" ,
131+ "--unshare-ipc" ,
132+ "--unshare-pid" ,
133+ "--unshare-uts" ,
134+ "--unshare-cgroup-try" ,
135+ "--ro-bind" , "/" , "/" ,
136+ "--proc" , "/proc" ,
137+ "--dev" , "/dev" ,
138+ "true" ,
139+ out : File ::NULL ,
140+ err : File ::NULL
141+ ) . and_return ( true )
142+
143+ expect ( sandbox_class . available? ) . to be ( true )
144+ end
145+
106146 it "reports setuid bubblewrap candidates" do
107147 allow ( File ) . to receive ( :stat ) . and_call_original
108148 allow ( File ) . to receive ( :stat ) . with ( bubblewrap ) . and_return ( instance_double ( File ::Stat , setuid? : true ) )
@@ -125,7 +165,7 @@ def executable_candidate_paths = test_executable_candidate_paths
125165 let ( :sandbox_class ) { Class . new ( klass ) }
126166
127167 around do |example |
128- with_env ( GITHUB_ACTIONS : nil , ImageOS : nil , RUNNER_ENVIRONMENT : nil ) { example . run }
168+ with_env ( GITHUB_ACTIONS : nil , HOMEBREW_GITHUB_HOSTED_RUNNER : nil ) { example . run }
129169 end
130170
131171 def expect_sandbox_configuration_command ( sandbox_class , assignment , result :)
@@ -164,39 +204,130 @@ def expect_sandbox_configuration_command(sandbox_class, assignment, result:)
164204 sandbox_class . configure!
165205 end
166206
167- it "installs Bubblewrap with apt-get on default GitHub Actions Ubuntu runners " do
207+ it "installs Bubblewrap and configures Linux sandbox sysctls " do
168208 expect ( sandbox_class ) . to receive ( :bubblewrap_executable )
169209 . twice
170- . and_return ( nil , Pathname ( "/usr/bin/bwrap" ) )
171- expect ( sandbox_class ) . to receive ( :ohai ) . with ( "Installing Bubblewrap..." )
172- expect ( sandbox_class ) . to receive ( :system )
173- . with ( "sudo" , "apt-get" , "install" , "--yes" , "bubblewrap" )
174- . and_return ( true )
175- expect ( sandbox_class ) . not_to receive ( :ensure_sandbox_installed! )
210+ . and_return ( nil , Pathname ( HOMEBREW_PREFIX /"bin/bwrap" ) )
211+ expect ( sandbox_class ) . to receive ( :ensure_sandbox_installed! )
212+ . with ( install_from_tests : true )
176213 expect ( sandbox_class ) . to receive ( :ohai ) . with ( "Configuring Bubblewrap..." ) . ordered
177214 expect_sandbox_configuration_command ( sandbox_class , "kernel.unprivileged_userns_clone=1" , result : true )
178215 expect_sandbox_configuration_command ( sandbox_class , "user.max_user_namespaces=28633" , result : true )
179216 expect_sandbox_configuration_command ( sandbox_class , "kernel.apparmor_restrict_unprivileged_userns=0" ,
180217 result : false )
181218
182- with_env ( GITHUB_ACTIONS : "true" , ImageOS : "ubuntu24" , RUNNER_ENVIRONMENT : "github-hosted" ) do
183- sandbox_class . configure!
184- end
219+ sandbox_class . configure!
185220 end
221+ end
186222
187- it "installs Bubblewrap and configures Linux sandbox sysctls" do
223+ describe "::ensure_sandbox_installed!" do
224+ let ( :sandbox_class ) { Class . new ( klass ) }
225+
226+ around do |example |
227+ with_env ( GITHUB_ACTIONS : nil , HOMEBREW_GITHUB_HOSTED_RUNNER : nil ,
228+ HOMEBREW_INSTALLING_BUBBLEWRAP : nil , HOMEBREW_TESTS : nil ) { example . run }
229+ end
230+
231+ before do
232+ allow ( Homebrew ::EnvConfig ) . to receive ( :sandbox_linux? ) . and_return ( true )
233+ end
234+
235+ it "does nothing when Homebrew Bubblewrap is already available" do
236+ expect ( sandbox_class ) . to receive ( :bubblewrap_executable )
237+ . once
238+ . and_return ( Pathname ( HOMEBREW_PREFIX /"bin/bwrap" ) )
239+ expect ( Formula ) . not_to receive ( :[] )
240+ expect ( sandbox_class ) . not_to receive ( :which )
241+ expect ( sandbox_class ) . not_to receive ( :system )
242+
243+ sandbox_class . ensure_sandbox_installed!
244+ end
245+
246+ it "does nothing when system Bubblewrap is already available" do
247+ expect ( sandbox_class ) . to receive ( :bubblewrap_executable )
248+ . once
249+ . and_return ( Pathname ( "/usr/bin/bwrap" ) )
250+ expect ( Formula ) . not_to receive ( :[] )
251+ expect ( sandbox_class ) . not_to receive ( :which )
252+ expect ( sandbox_class ) . not_to receive ( :system )
253+
254+ sandbox_class . ensure_sandbox_installed!
255+ end
256+
257+ it "installs Bubblewrap with Homebrew before trying apt-get on GitHub Actions" do
188258 expect ( sandbox_class ) . to receive ( :bubblewrap_executable )
189259 . twice
190260 . and_return ( nil , Pathname ( HOMEBREW_PREFIX /"bin/bwrap" ) )
191- expect ( sandbox_class ) . to receive ( :ensure_sandbox_installed! )
192- . with ( install_from_tests : true )
193- expect ( sandbox_class ) . to receive ( :ohai ) . with ( "Configuring Bubblewrap..." ) . ordered
194- expect_sandbox_configuration_command ( sandbox_class , "kernel.unprivileged_userns_clone=1" , result : true )
195- expect_sandbox_configuration_command ( sandbox_class , "user.max_user_namespaces=28633" , result : true )
196- expect_sandbox_configuration_command ( sandbox_class , "kernel.apparmor_restrict_unprivileged_userns=0" ,
197- result : false )
261+ expect ( Formula ) . to receive ( :[] ) . with ( "bubblewrap" )
262+ . and_return ( instance_double ( Formula , ensure_installed! : nil ) )
263+ expect ( sandbox_class ) . not_to receive ( :which )
264+ expect ( sandbox_class ) . not_to receive ( :system )
198265
199- sandbox_class . configure!
266+ with_env ( GITHUB_ACTIONS : "true" , HOMEBREW_GITHUB_HOSTED_RUNNER : "1" ) do
267+ sandbox_class . ensure_sandbox_installed!
268+ end
269+ end
270+
271+ it "falls back to sudo apt-get on GitHub Actions Ubuntu when Homebrew Bubblewrap is unavailable" do
272+ expect ( sandbox_class ) . to receive ( :bubblewrap_executable )
273+ . twice
274+ . and_return ( nil )
275+ expect ( Formula ) . to receive ( :[] ) . with ( "bubblewrap" )
276+ . and_return ( instance_double ( Formula , ensure_installed! : nil ) )
277+ expect ( sandbox_class ) . to receive ( :which ) . with ( "apt-get" ) . and_return ( Pathname ( "/usr/bin/apt-get" ) )
278+ expect ( Process ) . to receive ( :euid ) . and_return ( 1000 )
279+ expect ( sandbox_class ) . to receive ( :ohai ) . with ( "Installing Bubblewrap..." )
280+ expect ( sandbox_class ) . to receive ( :system )
281+ . with ( "sudo" , "apt-get" , "install" , "--yes" , "bubblewrap" )
282+ . and_return ( true )
283+
284+ with_env ( GITHUB_ACTIONS : "true" , HOMEBREW_GITHUB_HOSTED_RUNNER : "1" ) do
285+ sandbox_class . ensure_sandbox_installed!
286+ end
287+ end
288+
289+ it "falls back to apt-get as root on GitHub Actions Ubuntu when Homebrew Bubblewrap is unavailable" do
290+ expect ( sandbox_class ) . to receive ( :bubblewrap_executable )
291+ . twice
292+ . and_return ( nil )
293+ expect ( Formula ) . to receive ( :[] ) . with ( "bubblewrap" )
294+ . and_return ( instance_double ( Formula , ensure_installed! : nil ) )
295+ expect ( sandbox_class ) . to receive ( :which ) . with ( "apt-get" ) . and_return ( Pathname ( "/usr/bin/apt-get" ) )
296+ expect ( Process ) . to receive ( :euid ) . and_return ( 0 )
297+ expect ( sandbox_class ) . to receive ( :ohai ) . with ( "Installing Bubblewrap..." )
298+ expect ( sandbox_class ) . to receive ( :system )
299+ . with ( "apt-get" , "install" , "--yes" , "bubblewrap" )
300+ . and_return ( true )
301+
302+ with_env ( GITHUB_ACTIONS : "true" , HOMEBREW_GITHUB_HOSTED_RUNNER : "1" ) do
303+ sandbox_class . ensure_sandbox_installed!
304+ end
305+ end
306+
307+ it "does not fall back to apt-get outside GitHub Actions Ubuntu" do
308+ expect ( sandbox_class ) . to receive ( :bubblewrap_executable )
309+ . twice
310+ . and_return ( nil , nil )
311+ expect ( Formula ) . to receive ( :[] ) . with ( "bubblewrap" )
312+ . and_return ( instance_double ( Formula , ensure_installed! : nil ) )
313+ expect ( sandbox_class ) . not_to receive ( :which )
314+ expect ( sandbox_class ) . not_to receive ( :system )
315+
316+ with_env ( GITHUB_ACTIONS : "true" ) do
317+ sandbox_class . ensure_sandbox_installed!
318+ end
319+ end
320+
321+ it "does not fall back to apt-get outside GitHub Actions" do
322+ expect ( sandbox_class ) . to receive ( :bubblewrap_executable )
323+ . twice
324+ . and_return ( nil , nil )
325+ expect ( Formula ) . to receive ( :[] ) . with ( "bubblewrap" )
326+ . and_return ( instance_double ( Formula , ensure_installed! : nil ) )
327+ expect ( sandbox_class ) . not_to receive ( :which )
328+ expect ( sandbox_class ) . not_to receive ( :system )
329+
330+ sandbox_class . ensure_sandbox_installed!
200331 end
201332 end
202333
0 commit comments