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