risedev_config/
main.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(clippy::needless_question_mark)]
16
17use std::io::{BufRead, BufReader, BufWriter, Write};
18
19use anyhow::{Context, Result};
20use clap::{Parser, Subcommand, ValueEnum};
21use console::style;
22use dialoguer::MultiSelect;
23use enum_iterator::{Sequence, all};
24use fs_err::OpenOptions;
25use itertools::Itertools;
26
27#[derive(Parser)]
28#[clap(author, version, about, long_about = None)]
29#[clap(propagate_version = true)]
30#[clap(infer_subcommands = true)]
31pub struct RiseDevConfigOpts {
32    #[clap(subcommand)]
33    command: Option<Commands>,
34    #[clap(short, long)]
35    file: String,
36}
37
38#[derive(Subcommand)]
39#[clap(infer_subcommands = true)]
40enum Commands {
41    /// Enable one component
42    Enable {
43        /// Component to enable
44        #[clap(value_enum)]
45        component: Components,
46    },
47    /// Disable one component
48    Disable {
49        /// Component to disable
50        #[clap(value_enum)]
51        component: Components,
52    },
53    /// Use default configuration
54    Default,
55}
56
57#[allow(clippy::enum_variant_names)]
58#[derive(Clone, Copy, Debug, Sequence, PartialEq, Eq, ValueEnum)]
59pub enum Components {
60    #[clap(name = "minio")]
61    Minio,
62    Lakekeeper,
63    Hdfs,
64    PrometheusAndGrafana,
65    Pubsub,
66    Redis,
67    Tracing,
68    RustComponents,
69    UseSystem,
70    BuildConnectorNode,
71    Dashboard,
72    Release,
73    Sanitizer,
74    DynamicLinking,
75    HummockTrace,
76    Coredump,
77    NoBacktrace,
78    Udf,
79    NoDefaultFeatures,
80    Moat,
81    DataFusion,
82    Adbc,
83}
84
85impl Components {
86    pub fn title(&self) -> String {
87        match self {
88            Self::Minio => "[Component] Hummock: MinIO + MinIO-CLI",
89            Self::Lakekeeper => "[Component] Apache Iceberg: Lakekeeper REST Catalog",
90            Self::Hdfs => "[Component] Hummock: Hdfs Backend",
91            Self::PrometheusAndGrafana => "[Component] Metrics: Prometheus + Grafana",
92            Self::Pubsub => "[Component] Google Pubsub",
93            Self::Redis => "[Component] Redis",
94            Self::BuildConnectorNode => "[Build] Build RisingWave Connector (Java)",
95            Self::RustComponents => "[Build] Rust components",
96            Self::UseSystem => "[Build] Use system RisingWave",
97            Self::Dashboard => "[Build] Dashboard",
98            Self::Tracing => "[Component] Tracing: Grafana Tempo",
99            Self::Release => "[Build] Enable release mode",
100            Self::Sanitizer => "[Build] Enable sanitizer",
101            Self::DynamicLinking => "[Build] Enable dynamic linking",
102            Self::HummockTrace => "[Build] Hummock Trace",
103            Self::Coredump => "[Runtime] Enable coredump",
104            Self::NoBacktrace => "[Runtime] Disable backtrace",
105            Self::Udf => "[Build] Enable UDF",
106            Self::NoDefaultFeatures => "[Build] Disable default features",
107            Self::Moat => "[Component] Enable Moat",
108            Self::DataFusion => "[Build] Enable DataFusion",
109            Self::Adbc => "[Component] ADBC Snowflake Driver",
110        }
111        .into()
112    }
113
114    pub fn description(&self) -> String {
115        match self {
116            Self::Minio => {
117                "
118Required by Hummock state store."
119            }
120            Self::Lakekeeper => {
121                "
122Required if you want to use Apache Iceberg REST Catalog.
123Provides catalog and metadata management for Apache Iceberg tables."
124            }
125            Self::Hdfs => {
126                "
127Required by Hummock state store."
128            }
129            Self::PrometheusAndGrafana => {
130                "
131Required if you want to view metrics."
132            }
133            Self::Pubsub => {
134                "
135Required if you want to create source from Emulated Google Pub/sub.
136                "
137            }
138            Self::RustComponents => {
139                "
140Required if you want to build compute-node and meta-node.
141Otherwise you will need to enable `USE_SYSTEM_RISINGWAVE`, or
142manually download a binary and copy it to RiseDev directory."
143            }
144            Self::UseSystem => {
145                "
146Use the RisingWave installed in the PATH, instead of building it
147from source. This implies `ENABLE_BUILD_RUST` to be false.
148                "
149            }
150            Self::Dashboard => {
151                "
152Required if you want to build dashboard from source.
153This is generally not the option you want to use to develop the
154dashboard. Instead, directly run `npm run dev` in the dashboard
155directory to start the development server, set the API endpoint
156to a running RisingWave cluster in the settings page.
157"
158            }
159            Self::Tracing => {
160                "
161Required if you want to use tracing. This option will help
162you download Grafana Tempo."
163            }
164            Self::Release => {
165                "
166Build RisingWave in release mode"
167            }
168            Self::Sanitizer => {
169                "
170With this option enabled, RiseDev will build Rust components
171with thread sanitizer. The built binaries will be at
172`target/<arch-triple>/(debug|release)` instead of simply at
173`target/debug`. RiseDev will help link binaries when starting
174a dev cluster.
175"
176            }
177            Self::Redis => {
178                "
179Required if you want to sink data to redis.
180                "
181            }
182            Self::BuildConnectorNode => {
183                "
184Required if you want to build Connector Node from source locally.
185                "
186            }
187            Self::DynamicLinking => {
188                "
189With this option enabled, RiseDev will use dynamic linking when
190building Rust components. This can speed up the build process,
191but you might need the expertise to install dependencies correctly.
192                "
193            }
194            Self::HummockTrace => {
195                "
196With this option enabled, RiseDev will enable tracing for Hummock.
197See storage/hummock_trace for details.
198                "
199            }
200            Self::Coredump => {
201                "
202With this option enabled, RiseDev will unlimit the size of core
203files before launching RisingWave. On Apple Silicon platforms,
204the binaries will also be codesigned with `get-task-allow` enabled.
205As a result, RisingWave will dump the core on panics.
206                "
207            }
208            Self::NoBacktrace => {
209                "
210With this option enabled, RiseDev will not set `RUST_BACKTRACE` when launching nodes.
211                "
212            }
213            Self::Udf => {
214                "
215Add --features udf to build command (by default disabled).
216Required if you want to support UDF."
217            }
218            Self::NoDefaultFeatures => {
219                "
220Add --no-default-features to build command.
221Currently, default features are: rw-static-link, all-connectors
222"
223            }
224            Self::Moat => {
225                "
226Enable Moat as distributed hybrid cache service."
227            }
228            Self::DataFusion => {
229                "
230Enable DataFusion as the optional query engine for Iceberg tables."
231            }
232            Self::Adbc => {
233                "
234Enable ADBC (Arrow Database Connectivity) Snowflake driver support.
235Required if you want to use ADBC Snowflake source.
236This will download the ADBC Snowflake driver shared library (.so/.dylib)."
237            }
238        }
239        .into()
240    }
241
242    pub fn from_env(env: impl AsRef<str>) -> Option<Self> {
243        match env.as_ref() {
244            "ENABLE_MINIO" => Some(Self::Minio),
245            "ENABLE_LAKEKEEPER" => Some(Self::Lakekeeper),
246            "ENABLE_HDFS" => Some(Self::Hdfs),
247            "ENABLE_PROMETHEUS_GRAFANA" => Some(Self::PrometheusAndGrafana),
248            "ENABLE_PUBSUB" => Some(Self::Pubsub),
249            "ENABLE_BUILD_RUST" => Some(Self::RustComponents),
250            "USE_SYSTEM_RISINGWAVE" => Some(Self::UseSystem),
251            "ENABLE_BUILD_DASHBOARD" => Some(Self::Dashboard),
252            "ENABLE_COMPUTE_TRACING" => Some(Self::Tracing),
253            "ENABLE_RELEASE_PROFILE" => Some(Self::Release),
254            "ENABLE_DYNAMIC_LINKING" => Some(Self::DynamicLinking),
255            "ENABLE_SANITIZER" => Some(Self::Sanitizer),
256            "ENABLE_REDIS" => Some(Self::Redis),
257            "ENABLE_BUILD_RW_CONNECTOR" => Some(Self::BuildConnectorNode),
258            "ENABLE_HUMMOCK_TRACE" => Some(Self::HummockTrace),
259            "ENABLE_COREDUMP" => Some(Self::Coredump),
260            "DISABLE_BACKTRACE" => Some(Self::NoBacktrace),
261            "ENABLE_UDF" => Some(Self::Udf),
262            "DISABLE_DEFAULT_FEATURES" => Some(Self::NoDefaultFeatures),
263            "ENABLE_MOAT" => Some(Self::Moat),
264            "ENABLE_DATAFUSION" => Some(Self::DataFusion),
265            "ENABLE_ADBC" => Some(Self::Adbc),
266            _ => None,
267        }
268    }
269
270    pub fn env(&self) -> String {
271        match self {
272            Self::Minio => "ENABLE_MINIO",
273            Self::Lakekeeper => "ENABLE_LAKEKEEPER",
274            Self::Hdfs => "ENABLE_HDFS",
275            Self::PrometheusAndGrafana => "ENABLE_PROMETHEUS_GRAFANA",
276            Self::Pubsub => "ENABLE_PUBSUB",
277            Self::Redis => "ENABLE_REDIS",
278            Self::RustComponents => "ENABLE_BUILD_RUST",
279            Self::UseSystem => "USE_SYSTEM_RISINGWAVE",
280            Self::Dashboard => "ENABLE_BUILD_DASHBOARD",
281            Self::Tracing => "ENABLE_COMPUTE_TRACING",
282            Self::Release => "ENABLE_RELEASE_PROFILE",
283            Self::Sanitizer => "ENABLE_SANITIZER",
284            Self::BuildConnectorNode => "ENABLE_BUILD_RW_CONNECTOR",
285            Self::DynamicLinking => "ENABLE_DYNAMIC_LINKING",
286            Self::HummockTrace => "ENABLE_HUMMOCK_TRACE",
287            Self::Coredump => "ENABLE_COREDUMP",
288            Self::NoBacktrace => "DISABLE_BACKTRACE",
289            Self::Udf => "ENABLE_UDF",
290            Self::NoDefaultFeatures => "DISABLE_DEFAULT_FEATURES",
291            Self::Moat => "ENABLE_MOAT",
292            Self::DataFusion => "ENABLE_DATAFUSION",
293            Self::Adbc => "ENABLE_ADBC",
294        }
295        .into()
296    }
297
298    pub fn default_enabled() -> &'static [Self] {
299        &[Self::RustComponents]
300    }
301}
302
303fn configure(chosen: &[Components]) -> Result<Option<Vec<Components>>> {
304    println!("=== Configure RiseDev ===");
305
306    let all_components = all::<Components>().collect_vec();
307
308    const ITEMS_PER_PAGE: usize = 6;
309
310    let items = all_components
311        .iter()
312        .map(|c| {
313            let title = c.title();
314            let desc = style(
315                ("\n".to_owned() + c.description().trim())
316                    .split('\n')
317                    .join("\n      "),
318            )
319            .dim();
320
321            (format!("{title}{desc}",), chosen.contains(c))
322        })
323        .collect_vec();
324
325    let Some(chosen_indices) = MultiSelect::new()
326        .with_prompt(
327            format!(
328                "RiseDev includes several components. You can select the ones you need, so as to reduce build time\n\n{}: navigate\n{}: confirm and save   {}: quit without saving\n\nPick items with {}",
329                style("↑ / ↓ / ← / → ").reverse(),
330                style("Enter").reverse(),
331                style("Esc / q").reverse(),
332                style("Space").reverse(),
333            )
334        )
335        .items_checked(items)
336        .max_length(ITEMS_PER_PAGE)
337        .interact_opt()? else {
338        return Ok(None);
339    };
340
341    let chosen = chosen_indices
342        .into_iter()
343        .map(|i| all_components[i])
344        .collect_vec();
345
346    Ok(Some(chosen))
347}
348
349fn main() -> Result<()> {
350    let opts = RiseDevConfigOpts::parse();
351    let file_path = opts.file;
352
353    let chosen = {
354        match OpenOptions::new().read(true).open(&file_path) {
355            Ok(file) => {
356                let reader = BufReader::new(file);
357                let mut enabled = vec![];
358                for line in reader.lines() {
359                    let line = line?;
360                    if line.trim().is_empty() || line.trim().starts_with('#') {
361                        continue;
362                    }
363                    let Some((component, val)) = line.split_once('=') else {
364                        println!("invalid config line {}, discarded", line);
365                        continue;
366                    };
367                    if component == "RISEDEV_CONFIGURED" {
368                        continue;
369                    }
370                    match Components::from_env(component) {
371                        Some(component) => {
372                            if val == "true" {
373                                enabled.push(component);
374                            }
375                        }
376                        None => {
377                            println!("unknown configure {}, discarded", component);
378                            continue;
379                        }
380                    }
381                }
382                enabled
383            }
384            _ => {
385                println!(
386                    "RiseDev component config not found, generating {}",
387                    file_path
388                );
389                Components::default_enabled().to_vec()
390            }
391        }
392    };
393
394    let chosen = match &opts.command {
395        Some(Commands::Default) => {
396            println!("Using default config");
397            Components::default_enabled().to_vec()
398        }
399        Some(Commands::Enable { component }) => {
400            let mut chosen = chosen;
401            chosen.push(*component);
402            chosen
403        }
404        Some(Commands::Disable { component }) => {
405            chosen.into_iter().filter(|x| x != component).collect()
406        }
407        None => match configure(&chosen)? {
408            Some(chosen) => chosen,
409            None => {
410                println!("Quit without saving");
411                println!("=========================");
412                return Ok(());
413            }
414        },
415    };
416
417    println!("=== Enabled Components ===");
418    for component in all::<Components>() {
419        println!(
420            "{}: {}",
421            component.title(),
422            if chosen.contains(&component) {
423                style("enabled").green()
424            } else {
425                style("disabled").dim()
426            }
427        );
428    }
429
430    println!("Configuration saved at {}", file_path);
431    println!("=========================");
432
433    let mut file = BufWriter::new(
434        OpenOptions::new()
435            .write(true)
436            .truncate(true)
437            .create(true)
438            .open(&file_path)
439            .context(format!("failed to open component config at {}", file_path))?,
440    );
441
442    writeln!(file, "RISEDEV_CONFIGURED=true")?;
443    writeln!(file)?;
444
445    for component in all::<Components>() {
446        writeln!(file, "# {}", component.title())?;
447        writeln!(
448            file,
449            "# {}",
450            component.description().trim().split('\n').join("\n# ")
451        )?;
452        if chosen.contains(&component) {
453            writeln!(file, "{}=true", component.env())?;
454        } else {
455            writeln!(file, "# {}=true", component.env())?;
456        }
457        writeln!(file)?;
458    }
459
460    file.flush()?;
461
462    println!(
463        "RiseDev will {} the components you've enabled.",
464        style("only download").bold()
465    );
466    println!(
467        "If you want to use these components, please {} in {} to start that component.",
468        style("modify the cluster config").yellow().bold(),
469        style("risedev.yml").bold(),
470    );
471    println!("See CONTRIBUTING.md or RiseDev's readme for more information.");
472
473    Ok(())
474}