flowey_lib_common/
resolve_protoc.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Download a copy of `protoc` for the current platform or use a local copy.
5
6use flowey::node::prelude::*;
7
8#[derive(Serialize, Deserialize)]
9pub struct ProtocPackage {
10    pub protoc_bin: PathBuf,
11    pub include_dir: PathBuf,
12}
13
14/// Resolve protoc paths from a base directory and validate that they exist.
15/// If make_executable is true, this function will attempt to make the protoc binary executable.
16fn resolve_protoc_from_dir(
17    rt: &mut RustRuntimeServices<'_>,
18    base_dir: &Path,
19    make_executable: bool,
20) -> anyhow::Result<ProtocPackage> {
21    let protoc_bin = base_dir
22        .join("bin")
23        .join(rt.platform().binary("protoc"))
24        .absolute()?;
25
26    if !protoc_bin.exists() {
27        anyhow::bail!("protoc binary not found at {}", protoc_bin.display())
28    }
29
30    let protoc_bin_executable = protoc_bin.is_executable()?;
31    if !protoc_bin_executable && !make_executable {
32        anyhow::bail!(
33            "protoc binary at {} is not executable",
34            protoc_bin.display()
35        );
36    }
37
38    if make_executable {
39        protoc_bin.make_executable()?;
40    }
41
42    let include_dir = base_dir.join("include").absolute()?;
43    if !include_dir.exists() {
44        anyhow::bail!(
45            "protoc include directory not found at {}",
46            include_dir.display()
47        )
48    }
49
50    Ok(ProtocPackage {
51        protoc_bin,
52        include_dir,
53    })
54}
55
56flowey_request! {
57    pub enum Request {
58        /// Use a locally downloaded protoc
59        LocalPath(ReadVar<PathBuf>),
60        /// What version to download (e.g: 27.1)
61        Version(String),
62        /// Return paths to items in the protoc package
63        Get(WriteVar<ProtocPackage>),
64    }
65}
66
67new_flow_node!(struct Node);
68
69impl FlowNode for Node {
70    type Request = Request;
71
72    fn imports(ctx: &mut ImportCtx<'_>) {
73        ctx.import::<crate::install_dist_pkg::Node>();
74        ctx.import::<crate::download_gh_release::Node>();
75        ctx.import::<crate::cache::Node>();
76    }
77
78    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
79        let mut version = None;
80        let mut local_path: Option<ReadVar<PathBuf>> = None;
81        let mut get_reqs = Vec::new();
82
83        for req in requests {
84            match req {
85                Request::LocalPath(path) => {
86                    if local_path.is_some() {
87                        anyhow::bail!("Duplicate LocalPath requests")
88                    }
89                    local_path = Some(path);
90                }
91                Request::Version(v) => same_across_all_reqs("Version", &mut version, v)?,
92                Request::Get(v) => get_reqs.push(v),
93            }
94        }
95
96        if version.is_some() && local_path.is_some() {
97            anyhow::bail!("Cannot specify both Version and LocalPath requests");
98        }
99
100        if version.is_none() && local_path.is_none() {
101            anyhow::bail!("Must specify a Version or LocalPath request");
102        }
103
104        // -- end of req processing -- //
105
106        if get_reqs.is_empty() {
107            return Ok(());
108        }
109
110        if let Some(local_path) = local_path {
111            ctx.emit_rust_step("use local protoc", |ctx| {
112                let get_reqs = get_reqs.claim(ctx);
113                let local_path = local_path.claim(ctx);
114                move |rt| {
115                    let local_path = rt.read(local_path);
116                    log::info!("using protoc from base path {}", local_path.display());
117
118                    // If a local path is specified, assume protoc is already executable. This is necessary because a
119                    // nix-shell is unable to change file permissions but the file will be executable.
120                    let pkg = resolve_protoc_from_dir(rt, &local_path, false)?;
121                    rt.write_all(get_reqs, &pkg);
122
123                    Ok(())
124                }
125            });
126
127            return Ok(());
128        }
129
130        let version = version.expect("local requests handled above");
131
132        let tag = format!("v{version}");
133        let file_name = format!(
134            "protoc-{}-{}.zip",
135            version,
136            match (ctx.platform(), ctx.arch()) {
137                // protoc is not currently available for windows aarch64,
138                // so emulate the x64 version
139                (FlowPlatform::Windows, _) => "win64",
140                (FlowPlatform::Linux(_), FlowArch::X86_64) => "linux-x86_64",
141                (FlowPlatform::Linux(_), FlowArch::Aarch64) => "linux-aarch_64",
142                (FlowPlatform::MacOs, FlowArch::X86_64) => "osx-x86_64",
143                (FlowPlatform::MacOs, FlowArch::Aarch64) => "osx-aarch_64",
144                (platform, arch) => anyhow::bail!("unsupported platform {platform} {arch}"),
145            }
146        );
147
148        let protoc_zip = ctx.reqv(|v| crate::download_gh_release::Request {
149            repo_owner: "protocolbuffers".into(),
150            repo_name: "protobuf".into(),
151            needs_auth: false,
152            tag: tag.clone(),
153            file_name: file_name.clone(),
154            path: v,
155        });
156
157        let extract_zip_deps = crate::_util::extract::extract_zip_if_new_deps(ctx);
158        ctx.emit_rust_step("unpack protoc", |ctx| {
159            let extract_zip_deps = extract_zip_deps.clone().claim(ctx);
160            let get_reqs = get_reqs.claim(ctx);
161            let protoc_zip = protoc_zip.claim(ctx);
162            move |rt| {
163                let protoc_zip = rt.read(protoc_zip);
164
165                let extract_dir = crate::_util::extract::extract_zip_if_new(
166                    rt,
167                    extract_zip_deps,
168                    &protoc_zip,
169                    &tag,
170                )?;
171
172                let pkg = resolve_protoc_from_dir(rt, &extract_dir, true)?;
173                rt.write_all(get_reqs, &pkg);
174
175                Ok(())
176            }
177        });
178
179        Ok(())
180    }
181}