3333def fetch_bundler_source (request : Request ) -> RequestOutput :
3434 """Resolve and process all bundler packages."""
3535 components : list [Component ] = []
36- environment_variables : list [EnvironmentVariable ] = (
37- _prepare_environment_variables_for_hermetic_build ()
38- )
3936 project_files : list [ProjectFile ] = []
4037 git_paths = []
4138
@@ -48,9 +45,11 @@ def fetch_bundler_source(request: Request) -> RequestOutput:
4845 )
4946 components .extend (_comps )
5047 git_paths .extend (_git_paths )
51- project_files .append (
52- _prepare_for_hermetic_build (request .source_dir , request .output_dir , git_paths )
48+
49+ environment_variables : list [EnvironmentVariable ] = (
50+ _prepare_environment_variables_for_hermetic_build (git_paths )
5351 )
52+ project_files .append (_prepare_for_hermetic_build (request .source_dir , request .output_dir ))
5453
5554 annotations = []
5655 if backend_annotation := create_backend_annotation (components , "bundler" ):
@@ -63,17 +62,17 @@ def fetch_bundler_source(request: Request) -> RequestOutput:
6362 )
6463
6564
66- # Aliases for git dependency name and git dependency name as
67- # it is written to file system:
65+ # Aliases for git dependency name, file system name, and remote URL:
6866DepName = str
6967FSDepName = str
68+ DepURL = str
7069
7170
7271def _resolve_bundler_package (
7372 package_dir : RootedPath ,
7473 output_dir : RootedPath ,
7574 binary_filters : BundlerBinaryFilters | None = None ,
76- ) -> tuple [list [Component ], list [tuple [DepName , FSDepName ]]]:
75+ ) -> tuple [list [Component ], list [tuple [DepName , FSDepName , DepURL ]]]:
7776 """Process a request for a single bundler package."""
7877 deps_dir = output_dir .join_within_root ("deps" , "bundler" )
7978 deps_dir .path .mkdir (parents = True , exist_ok = True )
@@ -104,7 +103,7 @@ def _resolve_bundler_package(
104103 else :
105104 properties = []
106105 if isinstance (dep , GitDependency ):
107- git_paths .append ((dep .name , dep .repo_name + "-" + dep .ref [:12 ]))
106+ git_paths .append ((dep .name , dep .repo_name + "-" + dep .ref [:12 ], str ( dep . url ) ))
108107
109108 c = Component (name = dep .name , version = dep .version , purl = dep .purl , properties = properties )
110109 components .append (c )
@@ -183,16 +182,50 @@ def _get_repo_name_from_origin_remote(package_dir: RootedPath) -> str:
183182 return str (resolved_path )
184183
185184
186- def _prepare_environment_variables_for_hermetic_build () -> list [EnvironmentVariable ]:
187- return [
185+ def _prepare_environment_variables_for_hermetic_build (
186+ git_paths : list [tuple [DepName , FSDepName , DepURL ]] | None = None ,
187+ ) -> list [EnvironmentVariable ]:
188+ env_vars = [
188189 # Contains path to a directory where a new config could be found.
189190 EnvironmentVariable (name = "BUNDLE_APP_CONFIG" , value = "${output_dir}/" + CONFIG_OVERRIDE ),
190191 ]
191-
192-
193- def _prepare_for_hermetic_build (
194- source_dir : RootedPath , output_dir : RootedPath , git_paths : list | None = None
195- ) -> ProjectFile :
192+ if git_paths :
193+ # Redirect git remote URLs to pre-fetched local clones via git's
194+ # GIT_CONFIG_COUNT/KEY/VALUE mechanism. This injects url.insteadOf
195+ # entries without replacing the global git config.
196+ # See: https://git-scm.com/docs/git-config#ENVIRONMENT
197+ _check_for_duplicate_git_urls (git_paths )
198+ env_vars .append (EnvironmentVariable (name = "GIT_CONFIG_COUNT" , value = str (len (git_paths ))))
199+ for idx , (_packname , dirname , url ) in enumerate (git_paths ):
200+ clone_file_url = "file://${output_dir}/deps/bundler/" + dirname + "/"
201+ env_vars .append (
202+ EnvironmentVariable (
203+ name = f"GIT_CONFIG_KEY_{ idx } " ,
204+ value = f"url.{ clone_file_url } .insteadOf" ,
205+ )
206+ )
207+ env_vars .append (EnvironmentVariable (name = f"GIT_CONFIG_VALUE_{ idx } " , value = url ))
208+ return env_vars
209+
210+
211+ def _check_for_duplicate_git_urls (
212+ git_paths : list [tuple [DepName , FSDepName , DepURL ]],
213+ ) -> None :
214+ """Raise if multiple git deps share the same URL with different revisions."""
215+ url_to_dirs : dict [str , set [str ]] = {}
216+ for _packname , dirname , url in git_paths :
217+ url_to_dirs .setdefault (url , set ()).add (dirname )
218+ for url , dirs in url_to_dirs .items ():
219+ if len (dirs ) > 1 :
220+ raise UnsupportedFeature (
221+ f"Multiple git dependencies point to the same repository ({ url } ) "
222+ f"but use different revisions: { ', ' .join (sorted (dirs ))} . "
223+ "This is not supported because git's url.insteadOf redirect "
224+ "can only map a repository URL to a single local clone."
225+ )
226+
227+
228+ def _prepare_for_hermetic_build (source_dir : RootedPath , output_dir : RootedPath ) -> ProjectFile :
196229 """Prepare a package for hermetic build by injecting necessary config."""
197230 potential_bundle_config = source_dir .join_within_root (".bundle/config" ).path
198231 hermetic_config = dedent (
@@ -205,30 +238,6 @@ def _prepare_for_hermetic_build(
205238 BUNDLE_VERSION: "system"
206239 """
207240 )
208- # Note: if a package depends on a git revision then the following variables
209- # are necessary for a hermetic build:
210- # BUNDLE_DISABLE_LOCAL_BRANCH_CHECK
211- # BUNDLE_DISABLE_LOCAL_REVISION_CHECK
212- # because otherwise some (potentially all, depending on exact set of
213- # ecosystem components versions, environment variables and celestial
214- # alignment) Bundler versions will try to fetch the latest changes of the
215- # remotes which may be present even when instructed not to with --local
216- # flag.
217- # See https://bundler.io/guides/git.html#local-git-repos for details.
218- # (or https://github.com/rubygems/bundler-site/blob/
219- # 9ff3b76e9866524ecefe165633ffb547f0004a99/source/guides/git.html.md
220- # if the link above ceases to exist).
221- if git_paths is not None :
222- hermetic_config += 'BUNDLE_DISABLE_LOCAL_BRANCH_CHECK: "true"\n '
223- hermetic_config += 'BUNDLE_DISABLE_LOCAL_REVISION_CHECK: "true"\n '
224- for packname , dirname in git_paths :
225- # "-" in variable names is deprecated in Bundler and now generates
226- # a warning and a suggestion to replace all dashes with triple
227- # underscores. Package names sometimes contain dashes:
228- varname = "BUNDLE_LOCAL." + packname .upper ().replace ("-" , "___" )
229- location = "${output_dir}/deps/bundler/" + dirname
230- config_entry = varname + f': "{ location } "'
231- hermetic_config += f"{ config_entry } \n "
232241 if potential_bundle_config .is_file ():
233242 config_data = potential_bundle_config .read_text ()
234243 config_data += hermetic_config
0 commit comments