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(e) => {
195                                    bail!(e.as_error_message(dylib, &realpath_with_links).await?)
196                                }
197                            })),
198                        );
199                    }
200                    _ => {}
201                }
202            }
203        }
204        return Ok(*ModuleResolveResult::modules_with_affecting_sources(
205            sources
206                .into_iter()
207                .map(|(key, source)| async move {
208                    Ok((
209                        RequestKey::new(key),
210                        ResolvedVc::upcast(RawModule::new(source).to_resolved().await?),
211                    ))
212                })
213                .try_join()
214                .await?,
215            affecting_paths
216                .into_iter()
217                .map(|p| async move {
218                    anyhow::Ok(ResolvedVc::upcast(FileSource::new(p).to_resolved().await?))
219                })
220                .try_join()
221                .await?,
222        ));
223    };
224    Ok(*ModuleResolveResult::unresolvable())
225}
226
227#[turbo_tasks::value]
228#[derive(Hash, Clone, Debug, ValueToString)]
229#[value_to_string("node-gyp in {context_dir} for {compile_target}")]
230pub struct NodeGypBuildReference {
231    pub context_dir: FileSystemPath,
232    collect_affecting_sources: bool,
233    pub compile_target: ResolvedVc<CompileTarget>,
234}
235
236#[turbo_tasks::value_impl]
237impl NodeGypBuildReference {
238    #[turbo_tasks::function]
239    pub fn new(
240        context_dir: FileSystemPath,
241        collect_affecting_sources: bool,
242        compile_target: ResolvedVc<CompileTarget>,
243    ) -> Vc<Self> {
244        Self::cell(NodeGypBuildReference {
245            context_dir,
246            collect_affecting_sources,
247            compile_target,
248        })
249    }
250}
251
252#[turbo_tasks::value_impl]
253impl ModuleReference for NodeGypBuildReference {
254    #[turbo_tasks::function]
255    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
256        resolve_node_gyp_build_files(
257            self.context_dir.clone(),
258            self.collect_affecting_sources,
259            self.compile_target,
260        )
261        .await
262    }
263
264    fn chunking_type(&self) -> Option<ChunkingType> {
265        Some(ChunkingType::Traced {
266            mode: TracedMode::Transitive,
267        })
268    }
269}
270
271async fn resolve_node_gyp_build_files(
272    context_dir: FileSystemPath,
273    collect_affecting_sources: bool,
274    compile_target: ResolvedVc<CompileTarget>,
275) -> Result<Vc<ModuleResolveResult>> {
276    // TODO Proper parser
277    static GYP_BUILD_TARGET_NAME: LazyLock<Regex> = LazyLock::new(|| {
278        Regex::new(r#"['"]target_name['"]\s*:\s*(?:"(.*?)"|'(.*?)')"#)
279            .expect("create napi_build_version regex failed")
280    });
281    let binding_gyp_pat = Pattern::new(Pattern::Constant(rcstr!("binding.gyp")));
282    let gyp_file = resolve_raw(
283        context_dir.clone(),
284        binding_gyp_pat,
285        collect_affecting_sources,
286        true,
287    );
288    let gyp_file = gyp_file.await?;
289    let mut primary_sources = gyp_file.primary_sources();
290    if let (Some(binding_gyp), None) = (primary_sources.next(), primary_sources.next()) {
291        let mut merged_affecting_sources = if collect_affecting_sources {
292            gyp_file.get_affecting_sources().collect::<Vec<_>>()
293        } else {
294            Vec::new()
295        };
296        if let AssetContent::File(file) = &*binding_gyp.content().await?
297            && let FileContent::Content(config_file) = &*file.await?
298            && let Some(captured) = GYP_BUILD_TARGET_NAME.captures(&config_file.content().to_str()?)
299        {
300            let mut resolved: FxIndexMap<RcStr, ResolvedVc<Box<dyn Source>>> =
301                FxIndexMap::with_capacity_and_hasher(captured.len(), Default::default());
302            for found in captured.iter().skip(1).flatten() {
303                let name = found.as_str();
304                let target_path = context_dir.join("build/Release")?;
305                let resolved_prebuilt_file = resolve_raw(
306                    target_path,
307                    Pattern::new(Pattern::Constant(format!("{name}.node").into())),
308                    collect_affecting_sources,
309                    true,
310                )
311                .await?;
312                if let Some((_, ResolveResultItem::Source(source))) =
313                    resolved_prebuilt_file.primary.first()
314                {
315                    resolved.insert(format!("build/Release/{name}.node").into(), *source);
316                    if collect_affecting_sources {
317                        merged_affecting_sources
318                            .extend(resolved_prebuilt_file.affecting_sources.iter().copied());
319                    }
320                }
321            }
322            if !resolved.is_empty() {
323                return Ok(*ModuleResolveResult::modules_with_affecting_sources(
324                    resolved
325                        .into_iter()
326                        .map(|(key, source)| async move {
327                            Ok((
328                                RequestKey::new(key),
329                                ResolvedVc::upcast(RawModule::new(*source).to_resolved().await?),
330                            ))
331                        })
332                        .try_join()
333                        .await?,
334                    merged_affecting_sources,
335                ));
336            }
337        }
338    }
339    let compile_target = compile_target.await?;
340    let arch = compile_target.arch;
341    let platform = compile_target.platform;
342    let prebuilt_dir = format!("{platform}-{arch}");
343    Ok(resolve_raw(
344        context_dir,
345        Pattern::new(Pattern::Concatenation(vec![
346            Pattern::Constant(format!("prebuilds/{prebuilt_dir}/").into()),
347            Pattern::Dynamic,
348            Pattern::Constant(rcstr!(".node")),
349        ])),
350        collect_affecting_sources,
351        true,
352    )
353    .as_raw_module_result())
354}
355
356#[turbo_tasks::value]
357#[derive(Hash, Clone, Debug, ValueToString)]
358#[value_to_string("bindings in {context_dir}")]
359pub struct NodeBindingsReference {
360    pub context_dir: FileSystemPath,
361    pub file_name: RcStr,
362    pub collect_affecting_sources: bool,
363}
364
365#[turbo_tasks::value_impl]
366impl NodeBindingsReference {
367    #[turbo_tasks::function]
368    pub fn new(
369        context_dir: FileSystemPath,
370        file_name: RcStr,
371        collect_affecting_sources: bool,
372    ) -> Vc<Self> {
373        Self::cell(NodeBindingsReference {
374            context_dir,
375            file_name,
376            collect_affecting_sources,
377        })
378    }
379}
380
381#[turbo_tasks::value_impl]
382impl ModuleReference for NodeBindingsReference {
383    #[turbo_tasks::function]
384    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
385        resolve_node_bindings_files(
386            self.context_dir.clone(),
387            self.file_name.clone(),
388            self.collect_affecting_sources,
389        )
390        .await
391    }
392
393    fn chunking_type(&self) -> Option<ChunkingType> {
394        Some(ChunkingType::Traced {
395            mode: TracedMode::Transitive,
396        })
397    }
398}
399
400async fn resolve_node_bindings_files(
401    context_dir: FileSystemPath,
402    file_name: RcStr,
403    collect_affecting_sources: bool,
404) -> Result<Vc<ModuleResolveResult>> {
405    static BINDINGS_TRY: LazyLock<[&'static str; 5]> = LazyLock::new(|| {
406        [
407            "build/bindings",
408            "build/Release",
409            "build/Release/bindings",
410            "out/Release/bindings",
411            "Release/bindings",
412        ]
413    });
414    let mut root_context_dir = context_dir;
415    loop {
416        let resolved = resolve_raw(
417            root_context_dir.clone(),
418            Pattern::new(Pattern::Constant(rcstr!("package.json"))),
419            collect_affecting_sources,
420            true,
421        )
422        .await?;
423        if let Some(asset) = resolved.first_source()
424            && let AssetContent::File(file) = &*asset.content().await?
425            && let FileContent::Content(_) = &*file.await?
426        {
427            break;
428        };
429        let current_context = root_context_dir.clone();
430        let parent = root_context_dir.parent();
431        if parent.path == current_context.path {
432            break;
433        }
434        root_context_dir = parent;
435    }
436
437    let try_path = |sub_path: RcStr| async move {
438        let path = root_context_dir.join(&sub_path)?;
439        Ok(
440            if matches!(*path.get_type().await?, FileSystemEntryType::File) {
441                Some((
442                    RequestKey::new(sub_path),
443                    ResolvedVc::upcast(
444                        RawModule::new(Vc::upcast(FileSource::new(path.clone())))
445                            .to_resolved()
446                            .await?,
447                    ),
448                ))
449            } else {
450                None
451            },
452        )
453    };
454
455    let modules = BINDINGS_TRY
456        .iter()
457        .map(|try_dir| try_path.clone()(format!("{}/{}", try_dir, file_name).into()))
458        .try_flat_join()
459        .await?;
460    Ok(*ModuleResolveResult::modules(modules))
461}