turbopack_resolve/
node_native_binding.rs

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