2121# Visit https://github.com/aboutcode-org/scancode.io for support and download.
2222
2323import atexit
24- import concurrent .futures
2524import logging
2625import shutil
2726import subprocess
@@ -71,7 +70,8 @@ def fetch_inputs(purl, output_dir):
7170 """
7271 Fetch the system specific binary and the exact source tree with the
7372 patches and configurations applied for the given input purl. Return a
74- tuple of (source_path, binary_path, output_format).
73+ tuple of (source_path, binary_path, output_format, error_message,
74+ warning_message).
7575 """
7676 data = get_package_data (purl )
7777 name = purl .name
@@ -80,49 +80,140 @@ def fetch_inputs(purl, output_dir):
8080 commit_hash = purl .qualifiers .get ("commit" , "" )
8181 system = purl .qualifiers .get ("system" , "" )
8282 user_output = purl .qualifiers .get ("output" , "" )
83+ error_message = ""
84+ warning_message = ""
8385
8486 output_format , path , release_commit_hash = get_nix_store_path (
8587 data , name , version , system , commit_hash , user_output
8688 )
8789
88- nix_bin_download_url = get_nix_download_url (path ) if path else ""
8990 concluded_commit_hash = release_commit_hash or commit_hash
9091
91- src_path = ""
9292 bin_path = ""
93+ nix_bin_download_url = get_nix_download_url (path ) if path else ""
94+ # Try to download from cache first
95+ if nix_bin_download_url :
96+ bin_path = utils .fetch_path (nix_bin_download_url )
9397
94- # Run the Docker source patching and the Binary download concurrently
95- with concurrent . futures . ThreadPoolExecutor ( max_workers = 2 ) as executor :
96- futures = {}
98+ if bin_path :
99+ logger . info ( f"Downloaded binary for { purl } to { bin_path } " )
100+ else :
97101 if concluded_commit_hash :
98- futures ["source" ] = executor .submit (
99- get_patched_source_with_docker ,
100- name ,
101- output_dir ,
102- system ,
103- concluded_commit_hash ,
102+ logger .info (
103+ f"Binary not found in cache for { purl } . Attempting local Nix build..."
104+ )
105+ bin_path = build_binary_with_docker (
106+ name , output_dir , system , concluded_commit_hash , output_format
104107 )
105- if nix_bin_download_url :
106- futures ["binary" ] = executor .submit (utils .fetch_path , nix_bin_download_url )
108+ if bin_path :
109+ logger .info (f"Successfully built binary for { purl } to { bin_path } " )
110+ warning_message = (
111+ f"Binary not found in cache for { purl } . Built locally using "
112+ f"commit { concluded_commit_hash } with a Linux-based Nix "
113+ f"Docker container."
114+ )
115+ logger .warning (warning_message )
116+ else :
117+ error_message = f"Failed to fetch or build the binary for { purl } "
118+ logger .error (error_message )
119+
120+ src_path = ""
121+ if concluded_commit_hash :
122+ src_path = get_patched_source_with_docker (
123+ name , output_dir , system , concluded_commit_hash
124+ )
107125
108- for key , future in futures .items ():
109- try :
110- result = future .result (timeout = 600 )
111- if key == "source" :
112- src_path = result
113- else :
114- bin_path = result
115- if bin_path :
116- logger .info (f"Downloaded binary for { purl } to { bin_path } " )
117- else :
118- logger .info (f"Unable to download the binary for { purl } " )
126+ return src_path , bin_path , output_format , error_message , warning_message
127+
128+
129+ def build_binary_with_docker (name , output_dir , system , commit_hash , output_format ):
130+ """
131+ Fetch a Nix package and build its binary from source using Docker.
132+ Exports the resulting store path as a .nar file for standard extraction.
133+ """
134+ nar_filename = f"{ name } -bin.nar"
135+ extracted_path = Path (output_dir ) / nar_filename
136+ absolute_out_dir = str (Path (output_dir ).resolve ())
137+
138+ # Handle architecture and system incompatibilities
139+ target_os = system .split ("-" )[- 1 ] if "-" in system else system
140+ if target_os and target_os != "linux" :
141+ logger .warning (
142+ f"SYSTEM BARRIER DETECTED: Target system '{ system } ' requires "
143+ f"OS-specific SDKs that cannot be evaluated inside the "
144+ f"Linux-based Nix Docker container. Defaulting the build to "
145+ f"the container's native Linux architecture."
146+ )
147+ system_config = ""
148+ else :
149+ system_config = (
150+ f'localSystem = builtins.currentSystem; crossSystem = "{ system } ";'
151+ )
119152
120- except concurrent .futures .TimeoutError :
121- logger .error (f"Timeout waiting for { key } to fetch (exceeded 600s)." )
122- except Exception as e :
123- logger .error (f"Failed to fetch { key } : { e } " )
153+ config_str = (
154+ "config = { "
155+ "allowBroken = true; "
156+ "allowUnfree = true; "
157+ "allowUnsupportedSystem = true; "
158+ "};"
159+ )
124160
125- return src_path , bin_path , output_format
161+ nixpkgs_import = (
162+ f'import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/'
163+ f'{ commit_hash } .tar.gz") {{ { system_config } { config_str } }}'
164+ )
165+
166+ # Defaulting to 'debug' if none is specified.
167+ effective_output = output_format or "debug"
168+
169+ # Fall back to the default target if the effective_output is not
170+ # defined in the recipe for this package.
171+ nix_expression = (
172+ f"let "
173+ f" pkgs = { nixpkgs_import } ; "
174+ f" target = pkgs.{ name } ; "
175+ f" hasIt = builtins.isAttrs target && "
176+ f'builtins.hasAttr "{ effective_output } " target; '
177+ f"in if hasIt then target.{ effective_output } else target"
178+ )
179+
180+ # Build the Nix package, verify it succeeded, and export the output as
181+ # a .nar file.
182+ container_script = f"""
183+ OUT_PATH=$(nix-build --no-out-link -E '{ nix_expression } ')
184+ if [ -z "$OUT_PATH" ] || [ ! -e "$OUT_PATH" ]; then
185+ echo "Error: nix-build failed to return a valid store path." >&2
186+ exit 1
187+ fi
188+ nix-store --dump "$OUT_PATH" > /build_output/{ nar_filename }
189+ """
190+
191+ cmd = [
192+ "docker" ,
193+ "run" ,
194+ "--rm" ,
195+ "-v" ,
196+ "nix-eval-cache:/nix" ,
197+ "-v" ,
198+ f"{ absolute_out_dir } :/build_output" ,
199+ "nixos/nix" ,
200+ "/bin/sh" ,
201+ "-c" ,
202+ container_script ,
203+ ]
204+
205+ task_description = f"Building ({ name } for { system } )"
206+
207+ try :
208+ subprocess .run (cmd , capture_output = True , text = True , check = True , timeout = 1800 ) # noqa: S603
209+ if extracted_path .exists ():
210+ return str (extracted_path )
211+ return ""
212+ except subprocess .CalledProcessError as e :
213+ logger .error (f"Failed: { task_description } with error: { e .stderr .strip ()} " )
214+ except subprocess .TimeoutExpired :
215+ logger .error (f"Failed: { task_description } with error: Process timed out" )
216+ return ""
126217
127218
128219def get_nix_store_path (data , name , version , system , commit_hash , user_output ):
@@ -150,9 +241,9 @@ def get_nix_store_path(data, name, version, system, commit_hash, user_output):
150241 if not commit_hash :
151242 raise Exception (
152243 "Please provide a 'commit' qualifier in the PURL "
153- "for Nix to determine the download URL."
244+ "for Nix to determine the download URL or build it locally ."
154245 )
155- raise Exception ( f"Unable to determine the download URL for { name } " )
246+ output_format = user_output or "debug"
156247
157248 return output_format , path , release_commit_hash
158249
@@ -328,7 +419,7 @@ def extract_nar_archive(archive_path, output_dir, output):
328419 "-v" ,
329420 f"{ output_dir } :/output" ,
330421 "nixos/nix" ,
331- "sh" ,
422+ "/bin/ sh" ,
332423 "-c" ,
333424 container_script ,
334425 ]
@@ -425,7 +516,7 @@ def get_patched_source_with_docker(name, output_dir, system, commit_hash):
425516 "-v" ,
426517 f"{ absolute_out_dir } :/build_output" ,
427518 "nixos/nix" ,
428- "sh" ,
519+ "/bin/ sh" ,
429520 "-c" ,
430521 container_script ,
431522 ]
0 commit comments