Skip to main content

turbopack_resolve/
node_native_binding.rs

1use std::sync::LazyLock;
2
3use anyhow::{Result, bail};
4use regex::Regex;
5use serde::Deserialize;
6use turbo_rcstr::{RcStr, rcstr};
7use turbo_tasks::{FxIndexMap, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc};
8use turbo_tasks_fs::{
9    DirectoryContent, DirectoryEntry, FileContent, FileSystemEntryType, FileSystemPath,
10    json::parse_json_rope_with_source_context,
11};
12use turbopack_core::{
13    asset::{Asset, AssetContent},
14    chunk::{ChunkingType, TracedMode},
15    file_source::FileSource,
16    raw_module::RawModule,
17    reference::ModuleReference,
18    resolve::{ModuleResolveResult, RequestKey, ResolveResultItem, pattern::Pattern, resolve_raw},
19    source::Source,
20    target::{CompileTarget, Platform},
21};
22
23#[derive(Deserialize, Debug)]
24struct NodePreGypConfigJson {
25    binary: NodePreGypConfig,
26}
27
28#[derive(Deserialize, Debug)]
29struct NodePreGypConfig {
30    module_name: String,
31    module_path: String,
32    napi_versions: Vec<u32>,
33}
34
35#[turbo_tasks::value]
36#[derive(Hash, Clone, Debug, ValueToString)]
37#[value_to_string("node-gyp in {context_dir} with {config_file_pattern} for {compile_target}")]
38pub struct NodePreGypConfigReference {
39    pub context_dir: FileSystemPath,
40    pub config_file_pattern: ResolvedVc<Pattern>,
41    pub compile_target: ResolvedVc<CompileTarget>,
42    pub collect_affecting_sources: bool,
43}
44
45#[turbo_tasks::value_impl]
46impl NodePreGypConfigReference {
47    #[turbo_tasks::function]
48    pub fn new(
49        context_dir: FileSystemPath,
50        config_file_pattern: ResolvedVc<Pattern>,
51        compile_target: ResolvedVc<CompileTarget>,
52        collect_affecting_sources: bool,
53    ) -> Vc<Self> {
54        Self::cell(NodePreGypConfigReference {
55            context_dir,
56            config_file_pattern,
57            compile_target,
58            collect_affecting_sources,
59        })
60    }
61}
62
63#[turbo_tasks::value_impl]
64impl ModuleReference for NodePreGypConfigReference {
65    #[turbo_tasks::function]
66    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
67        resolve_node_pre_gyp_files(
68            self.context_dir.clone(),
69            *self.config_file_pattern,
70            *self.compile_target,
71            self.collect_affecting_sources,
72        )
73        .await
74    }
75
76    fn chunking_type(&self) -> Option<ChunkingType> {
77        Some(ChunkingType::Traced {
78            mode: TracedMode::Transitive,
79        })
80    }
81}
82
83async fn resolve_node_pre_gyp_files(
84    context_dir: FileSystemPath,
85    config_file_pattern: Vc<Pattern>,
86    compile_target: Vc<CompileTarget>,
87    collect_affecting_sources: bool,
88) -> Result<Vc<ModuleResolveResult>> {
89    static NAPI_VERSION_TEMPLATE: LazyLock<Regex> = LazyLock::new(|| {
90        Regex::new(r"\{(napi_build_version|node_napi_label)\}")
91            .expect("create napi_build_version regex failed")
92    });
93    static PLATFORM_TEMPLATE: LazyLock<Regex> =
94        LazyLock::new(|| Regex::new(r"\{platform\}").expect("create node_platform regex failed"));
95    static ARCH_TEMPLATE: LazyLock<Regex> =
96        LazyLock::new(|| Regex::new(r"\{arch\}").expect("create node_arch regex failed"));
97    static LIBC_TEMPLATE: LazyLock<Regex> =
98        LazyLock::new(|| Regex::new(r"\{libc\}").expect("create node_libc regex failed"));
99    let config = resolve_raw(
100        context_dir,
101        config_file_pattern,
102        collect_affecting_sources,
103        true,
104    )
105    .await?;
106    let compile_target = compile_target.await?;
107    if let Some(config_asset) = config.first_source()
108        && let AssetContent::File(file) = &*config_asset.content().await?
109        && let FileContent::Content(config_file) = &*file.await?
110    {
111        let config_asset_ident = config_asset.ident().await?;
112        let config_file_path = &config_asset_ident.path;
113        let mut affecting_paths = vec![config_file_path.clone()];
114        let config_file_dir = config_file_path.parent();
115        let node_pre_gyp_config: NodePreGypConfigJson =
116            parse_json_rope_with_source_context(config_file.content())?;
117        let mut sources: FxIndexMap<RcStr, Vc<Box<dyn Source>>> = FxIndexMap::default();
118        for version in node_pre_gyp_config.binary.napi_versions.iter() {
119            let native_binding_path = NAPI_VERSION_TEMPLATE.replace(
120                node_pre_gyp_config.binary.module_path.as_str(),
121                version.to_string(),
122            );
123            let platform = compile_target.platform;
124            let native_binding_path =
125                PLATFORM_TEMPLATE.replace(&native_binding_path, platform.as_str());
126            let native_binding_path =
127                ARCH_TEMPLATE.replace(&native_binding_path, compile_target.arch.as_str());
128            let native_binding_path: RcStr = LIBC_TEMPLATE
129                .replace(
130                    &native_binding_path,
131                    // node-pre-gyp only cares about libc on linux
132                    if platform == Platform::Linux {
133                        compile_target.libc.as_str()
134                    } else {
135                        "unknown"
136                    },
137                )
138                .into();
139
140            // Find all dynamic libraries in the given directory.
141            if let DirectoryContent::Entries(entries) = &*config_file_dir
142                .join(&native_binding_path)?
143                .read_dir()
144                .await?
145            {
146                let extension = compile_target.dylib_ext();
147                for (key, entry) in entries.iter().filter(|(k, _)| k.ends_with(extension)) {
148                    if let DirectoryEntry::File(dylib) | DirectoryEntry::Symlink(dylib) = entry {
149                        sources.insert(
150                            format!("{native_binding_path}/{key}").into(),
151                            Vc::upcast(FileSource::new(dylib.clone())),
152                        );
153                    }
154                }
155            }
156
157            let node_file_path: RcStr = format!(
158                "{}/{}.node",
159                native_binding_path, node_pre_gyp_config.binary.module_name
160            )
161            .into();
162            let resolved_file_vc = config_file_dir.join(&node_file_path)?;
163            if *resolved_file_vc.get_type().await? == FileSystemEntryType::File {
164                sources.insert(
165                    node_file_path,
166                    Vc::upcast(FileSource::new(resolved_file_vc)),
167                );
168            }
169        }
170        if let DirectoryContent::Entries(entries) = &*config_file_dir
171            // TODO
172            // read the dependencies path from `bindings.gyp`
173            .join("deps/lib")?
174            .read_dir()
175            .await?
176        {
177            for (key, entry) in entries.iter() {
178                match entry {
179                    DirectoryEntry::File(dylib) => {
180                        sources.insert(
181                            format!("deps/lib/{key}").into(),
182                            Vc::upcast(FileSource::new(dylib.clone())),
183                        );
184                    }
185                    DirectoryEntry::Symlink(dylib) => {
186                        let realpath_with_links = dylib.realpath_with_links().await?;
187                        for symlink in realpath_with_links.symlinks.iter() {
188                            affecting_paths.push(symlink.clone());
189                        }
190                        sources.insert(
191                            format!("deps/lib/{key}").into(),
192                            Vc::upcast(FileSource::new(match &realpath_with_links.path_result {
193                                Ok(path) => path.clone(),
194                                Err(error) => bail!(error.clone()),
195                            })),
196                        );
197                    }
198                    _ => {}
199                }
200            }
201        }
202        return Ok(*ModuleResolveResult::modules_with_affecting_sources(
203            sources
204                .into_iter()
205                .map(async |(key, source)| {
206                    Ok((
207                        RequestKey::new(key),
208                        ResolvedVc::upcast(RawModule::new(source).to_resolved().await?),
209                    ))
210                })
211                .try_join()
212                .await?,
213            affecting_paths
214                .into_iter()
215                .map(async |p| Ok(ResolvedVc::upcast(FileSource::new(p).to_resolved().await?)))
216                .try_join()
217                .await?,
218        ));
219    };
220    Ok(*ModuleResolveResult::unresolvable())
221}
222
223#[turbo_tasks::value]
224#[derive(Hash, Clone, Debug, ValueToString)]
225#[value_to_string("node-gyp in {context_dir} for {compile_target}")]
226pub struct NodeGypBuildReference {
227    pub context_dir: FileSystemPath,
228    collect_affecting_sources: bool,
229    pub compile_target: ResolvedVc<CompileTarget>,
230}
231
232#[turbo_tasks::value_impl]
233impl NodeGypBuildReference {
234    #[turbo_tasks::function]
235    pub fn new(
236        context_dir: FileSystemPath,
237        collect_affecting_sources: bool,
238        compile_target: ResolvedVc<CompileTarget>,
239    ) -> Vc<Self> {
240        Self::cell(NodeGypBuildReference {
241            context_dir,
242            collect_affecting_sources,
243            compile_target,
244        })
245    }
246}
247
248#[turbo_tasks::value_impl]
249impl ModuleReference for NodeGypBuildReference {
250    #[turbo_tasks::function]
251    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
252        resolve_node_gyp_build_files(
253            self.context_dir.clone(),
254            self.collect_affecting_sources,
255            self.compile_target,
256        )
257        .await
258    }
259
260    fn chunking_type(&self) -> Option<ChunkingType> {
261        Some(ChunkingType::Traced {
262            mode: TracedMode::Transitive,
263        })
264    }
265}
266
267async fn resolve_node_gyp_build_files(
268    context_dir: FileSystemPath,
269    collect_affecting_sources: bool,
270    compile_target: ResolvedVc<CompileTarget>,
271) -> Result<Vc<ModuleResolveResult>> {
272    // TODO Proper parser
273    static GYP_BUILD_TARGET_NAME: LazyLock<Regex> = LazyLock::new(|| {
274        Regex::new(r#"['"]target_name['"]\s*:\s*(?:"(.*?)"|'(.*?)')"#)
275            .expect("create napi_build_version regex failed")
276    });
277    let binding_gyp_pat = Pattern::new(Pattern::Constant(rcstr!("binding.gyp")));
278    let gyp_file = resolve_raw(
279        context_dir.clone(),
280        binding_gyp_pat,
281        collect_affecting_sources,
282        true,
283    );
284    let gyp_file = gyp_file.await?;
285    let mut primary_sources = gyp_file.primary_sources();
286    if let (Some(binding_gyp), None) = (primary_sources.next(), primary_sources.next()) {
287        let mut merged_affecting_sources = if collect_affecting_sources {
288            gyp_file.get_affecting_sources().collect::<Vec<_>>()
289        } else {
290            Vec::new()
291        };
292        if let AssetContent::File(file) = &*binding_gyp.content().await?
293            && let FileContent::Content(config_file) = &*file.await?
294            && let Some(captured) = GYP_BUILD_TARGET_NAME.captures(&config_file.content().to_str()?)
295        {
296            let mut resolved: FxIndexMap<RcStr, ResolvedVc<Box<dyn Source>>> =
297                FxIndexMap::with_capacity_and_hasher(captured.len(), Default::default());
298            for found in captured.iter().skip(1).flatten() {
299                let name = found.as_str();
300                let target_path = context_dir.join("build/Release")?;
301                let resolved_prebuilt_file = resolve_raw(
302                    target_path,
303                    Pattern::new(Pattern::Constant(format!("{name}.node").into())),
304                    collect_affecting_sources,
305                    true,
306                )
307                .await?;
308                if let Some((_, ResolveResultItem::Source(source))) =
309                    resolved_prebuilt_file.primary.first()
310                {
311                    resolved.insert(format!("build/Release/{name}.node").into(), *source);
312                    if collect_affecting_sources {
313                        merged_affecting_sources
314                            .extend(resolved_prebuilt_file.affecting_sources.iter().copied());
315                    }
316                }
317            }
318            if !resolved.is_empty() {
319                return Ok(*ModuleResolveResult::modules_with_affecting_sources(
320                    resolved
321                        .into_iter()
322                        .map(async |(key, source)| {
323                            Ok((
324                                RequestKey::new(key),
325                                ResolvedVc::upcast(RawModule::new(*source).to_resolved().await?),
326                            ))
327                        })
328                        .try_join()
329                        .await?,
330                    merged_affecting_sources,
331                ));
332            }
333        }
334    }
335    let compile_target = compile_target.await?;
336    let arch = compile_target.arch;
337    let platform = compile_target.platform;
338    let prebuilt_dir = format!("{platform}-{arch}");
339    Ok(resolve_raw(
340        context_dir,
341        Pattern::new(Pattern::Concatenation(vec![
342            Pattern::Constant(format!("prebuilds/{prebuilt_dir}/").into()),
343            Pattern::Dynamic,
344            Pattern::Constant(rcstr!(".node")),
345        ])),
346        collect_affecting_sources,
347        true,
348    )
349    .as_raw_module_result())
350}
351
352#[turbo_tasks::value]
353#[derive(Hash, Clone, Debug, ValueToString)]
354#[value_to_string("bindings in {context_dir}")]
355pub struct NodeBindingsReference {
356    pub context_dir: FileSystemPath,
357    pub file_name: RcStr,
358    pub collect_affecting_sources: bool,
359}
360
361#[turbo_tasks::value_impl]
362impl NodeBindingsReference {
363    #[turbo_tasks::function]
364    pub fn new(
365        context_dir: FileSystemPath,
366        file_name: RcStr,
367        collect_affecting_sources: bool,
368    ) -> Vc<Self> {
369        Self::cell(NodeBindingsReference {
370            context_dir,
371            file_name,
372            collect_affecting_sources,
373        })
374    }
375}
376
377#[turbo_tasks::value_impl]
378impl ModuleReference for NodeBindingsReference {
379    #[turbo_tasks::function]
380    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
381        resolve_node_bindings_files(
382            self.context_dir.clone(),
383            self.file_name.clone(),
384            self.collect_affecting_sources,
385        )
386        .await
387    }
388
389    fn chunking_type(&self) -> Option<ChunkingType> {
390        Some(ChunkingType::Traced {
391            mode: TracedMode::Transitive,
392        })
393    }
394}
395
396async fn resolve_node_bindings_files(
397    context_dir: FileSystemPath,
398    file_name: RcStr,
399    collect_affecting_sources: bool,
400) -> Result<Vc<ModuleResolveResult>> {
401    static BINDINGS_TRY: LazyLock<[&'static str; 5]> = LazyLock::new(|| {
402        [
403            "build/bindings",
404            "build/Release",
405            "build/Release/bindings",
406            "out/Release/bindings",
407            "Release/bindings",
408        ]
409    });
410    let mut root_context_dir = context_dir;
411    loop {
412        let resolved = resolve_raw(
413            root_context_dir.clone(),
414            Pattern::new(Pattern::Constant(rcstr!("package.json"))),
415            collect_affecting_sources,
416            true,
417        )
418        .await?;
419        if let Some(asset) = resolved.first_source()
420            && let AssetContent::File(file) = &*asset.content().await?
421            && let FileContent::Content(_) = &*file.await?
422        {
423            break;
424        };
425        let current_context = root_context_dir.clone();
426        let parent = root_context_dir.parent();
427        if parent.path == current_context.path {
428            break;
429        }
430        root_context_dir = parent;
431    }
432
433    let try_path = async |sub_path: RcStr| {
434        let path = root_context_dir.join(&sub_path)?;
435        Ok(
436            if matches!(*path.get_type().await?, FileSystemEntryType::File) {
437                Some((
438                    RequestKey::new(sub_path),
439                    ResolvedVc::upcast(
440                        RawModule::new(Vc::upcast(FileSource::new(path.clone())))
441                            .to_resolved()
442                            .await?,
443                    ),
444                ))
445            } else {
446                None
447            },
448        )
449    };
450
451    let modules = BINDINGS_TRY
452        .iter()
453        .map(|try_dir| try_path(format!("{}/{}", try_dir, file_name).into()))
454        .try_flat_join()
455        .await?;
456    Ok(*ModuleResolveResult::modules(modules))
457}