risedev_config/
main.rs

1// Copyright 2025 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}
82
83impl Components {
84    pub fn title(&self) -> String {
85        match self {
86            Self::Minio => "[Component] Hummock: MinIO + MinIO-CLI",
87            Self::Lakekeeper => "[Component] Apache Iceberg: Lakekeeper REST Catalog",
88            Self::Hdfs => "[Component] Hummock: Hdfs Backend",
89            Self::PrometheusAndGrafana => "[Component] Metrics: Prometheus + Grafana",
90            Self::Pubsub => "[Component] Google Pubsub",
91            Self::Redis => "[Component] Redis",
92            Self::BuildConnectorNode => "[Build] Build RisingWave Connector (Java)",
93            Self::RustComponents => "[Build] Rust components",
94            Self::UseSystem => "[Build] Use system RisingWave",
95            Self::Dashboard => "[Build] Dashboard",
96            Self::Tracing => "[Component] Tracing: Grafana Tempo",
97            Self::Release => "[Build] Enable release mode",
98            Self::Sanitizer => "[Build] Enable sanitizer",
99            Self::DynamicLinking => "[Build] Enable dynamic linking",
100            Self::HummockTrace => "[Build] Hummock Trace",
101            Self::Coredump => "[Runtime] Enable coredump",
102            Self::NoBacktrace => "[Runtime] Disable backtrace",
103            Self::Udf => "[Build] Enable UDF",
104            Self::NoDefaultFeatures => "[Build] Disable default features",
105            Self::Moat => "[Component] Enable Moat",
106        }
107        .into()
108    }
109
110    pub fn description(&self) -> String {
111        match self {
112            Self::Minio => {
113                "
114Required by Hummock state store."
115            }
116            Self::Lakekeeper => {
117                "
118Required if you want to use Apache Iceberg REST Catalog.
119Provides catalog and metadata management for Apache Iceberg tables."
120            }
121            Self::Hdfs => {
122                "
123Required by Hummock state store."
124            }
125            Self::PrometheusAndGrafana => {
126                "
127Required if you want to view metrics."
128            }
129            Self::Pubsub => {
130                "
131Required if you want to create source from Emulated Google Pub/sub.
132                "
133            }
134            Self::RustComponents => {
135                "
136Required if you want to build compute-node and meta-node.
137Otherwise you will need to enable `USE_SYSTEM_RISINGWAVE`, or
138manually download a binary and copy it to RiseDev directory."
139            }
140            Self::UseSystem => {
141                "
142Use the RisingWave installed in the PATH, instead of building it
143from source. This implies `ENABLE_BUILD_RUST` to be false.
144                "
145            }
146            Self::Dashboard => {
147                "
148Required if you want to build dashboard from source.
149This is generally not the option you want to use to develop the
150dashboard. Instead, directly run `npm run dev` in the dashboard
151directory to start the development server, set the API endpoint
152to a running RisingWave cluster in the settings page.
153"
154            }
155            Self::Tracing => {
156                "
157Required if you want to use tracing. This option will help
158you download Grafana Tempo."
159            }
160            Self::Release => {
161                "
162Build RisingWave in release mode"
163            }
164            Self::Sanitizer => {
165                "
166With this option enabled, RiseDev will build Rust components
167with thread sanitizer. The built binaries will be at
168`target/<arch-triple>/(debug|release)` instead of simply at
169`target/debug`. RiseDev will help link binaries when starting
170a dev cluster.
171"
172            }
173            Self::Redis => {
174                "
175Required if you want to sink data to redis.
176                "
177            }
178            Self::BuildConnectorNode => {
179                "
180Required if you want to build Connector Node from source locally.
181                "
182            }
183            Self::DynamicLinking => {
184                "
185With this option enabled, RiseDev will use dynamic linking when
186building Rust components. This can speed up the build process,
187but you might need the expertise to install dependencies correctly.
188                "
189            }
190            Self::HummockTrace => {
191                "
192With this option enabled, RiseDev will enable tracing for Hummock.
193See storage/hummock_trace for details.
194                "
195            }
196            Self::Coredump => {
197                "
198With this option enabled, RiseDev will unlimit the size of core
199files before launching RisingWave. On Apple Silicon platforms,
200the binaries will also be codesigned with `get-task-allow` enabled.
201As a result, RisingWave will dump the core on panics.
202                "
203            }
204            Self::NoBacktrace => {
205                "
206With this option enabled, RiseDev will not set `RUST_BACKTRACE` when launching nodes.
207                "
208            }
209            Self::Udf => {
210                "
211Add --features udf to build command (by default disabled).
212Required if you want to support UDF."
213            }
214            Self::NoDefaultFeatures => {
215                "
216Add --no-default-features to build command.
217Currently, default features are: rw-static-link, all-connectors
218"
219            }
220            Self::Moat => {
221                "
222Enable Moat as distributed hybrid cache service."
223            }
224        }
225        .into()
226    }
227
228    pub fn from_env(env: impl AsRef<str>) -> Option<Self> {
229        match env.as_ref() {
230            "ENABLE_MINIO" => Some(Self::Minio),
231            "ENABLE_LAKEKEEPER" => Some(Self::Lakekeeper),
232            "ENABLE_HDFS" => Some(Self::Hdfs),
233            "ENABLE_PROMETHEUS_GRAFANA" => Some(Self::PrometheusAndGrafana),
234            "ENABLE_PUBSUB" => Some(Self::Pubsub),
235            "ENABLE_BUILD_RUST" => Some(Self::RustComponents),
236            "USE_SYSTEM_RISINGWAVE" => Some(Self::UseSystem),
237            "ENABLE_BUILD_DASHBOARD" => Some(Self::Dashboard),
238            "ENABLE_COMPUTE_TRACING" => Some(Self::Tracing),
239            "ENABLE_RELEASE_PROFILE" => Some(Self::Release),
240            "ENABLE_DYNAMIC_LINKING" => Some(Self::DynamicLinking),
241            "ENABLE_SANITIZER" => Some(Self::Sanitizer),
242            "ENABLE_REDIS" => Some(Self::Redis),
243            "ENABLE_BUILD_RW_CONNECTOR" => Some(Self::BuildConnectorNode),
244            "ENABLE_HUMMOCK_TRACE" => Some(Self::HummockTrace),
245            "ENABLE_COREDUMP" => Some(Self::Coredump),
246            "DISABLE_BACKTRACE" => Some(Self::NoBacktrace),
247            "ENABLE_UDF" => Some(Self::Udf),
248            "DISABLE_DEFAULT_FEATURES" => Some(Self::NoDefaultFeatures),
249            "ENABLE_MOAT" => Some(Self::Moat),
250            _ => None,
251        }
252    }
253
254    pub fn env(&self) -> String {
255        match self {
256            Self::Minio => "ENABLE_MINIO",
257            Self::Lakekeeper => "ENABLE_LAKEKEEPER",
258            Self::Hdfs => "ENABLE_HDFS",
259            Self::PrometheusAndGrafana => "ENABLE_PROMETHEUS_GRAFANA",
260            Self::Pubsub => "ENABLE_PUBSUB",
261            Self::Redis => "ENABLE_REDIS",
262            Self::RustComponents => "ENABLE_BUILD_RUST",
263            Self::UseSystem => "USE_SYSTEM_RISINGWAVE",
264            Self::Dashboard => "ENABLE_BUILD_DASHBOARD",
265            Self::Tracing => "ENABLE_COMPUTE_TRACING",
266            Self::Release => "ENABLE_RELEASE_PROFILE",
267            Self::Sanitizer => "ENABLE_SANITIZER",
268            Self::BuildConnectorNode => "ENABLE_BUILD_RW_CONNECTOR",
269            Self::DynamicLinking => "ENABLE_DYNAMIC_LINKING",
270            Self::HummockTrace => "ENABLE_HUMMOCK_TRACE",
271            Self::Coredump => "ENABLE_COREDUMP",
272            Self::NoBacktrace => "DISABLE_BACKTRACE",
273            Self::Udf => "ENABLE_UDF",
274            Self::NoDefaultFeatures => "DISABLE_DEFAULT_FEATURES",
275            Self::Moat => "ENABLE_MOAT",
276        }
277        .into()
278    }
279
280    pub fn default_enabled() -> &'static [Self] {
281        &[Self::RustComponents]
282    }
283}
284
285fn configure(chosen: &[Components]) -> Result<Option<Vec<Components>>> {
286    println!("=== Configure RiseDev ===");
287
288    let all_components = all::<Components>().collect_vec();
289
290    const ITEMS_PER_PAGE: usize = 6;
291
292    let items = all_components
293        .iter()
294        .map(|c| {
295            let title = c.title();
296            let desc = style(
297                ("\n".to_owned() + c.description().trim())
298                    .split('\n')
299                    .join("\n      "),
300            )
301            .dim();
302
303            (format!("{title}{desc}",), chosen.contains(c))
304        })
305        .collect_vec();
306
307    let Some(chosen_indices) = MultiSelect::new()
308        .with_prompt(
309            format!(
310                "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 {}",
311                style("↑ / ↓ / ← / → ").reverse(),
312                style("Enter").reverse(),
313                style("Esc / q").reverse(),
314                style("Space").reverse(),
315            )
316        )
317        .items_checked(&items)
318        .max_length(ITEMS_PER_PAGE)
319        .interact_opt()? else {
320        return Ok(None);
321    };
322
323    let chosen = chosen_indices
324        .into_iter()
325        .map(|i| all_components[i])
326        .collect_vec();
327
328    Ok(Some(chosen))
329}
330
331fn main() -> Result<()> {
332    let opts = RiseDevConfigOpts::parse();
333    let file_path = opts.file;
334
335    let chosen = {
336        match OpenOptions::new().read(true).open(&file_path) {
337            Ok(file) => {
338                let reader = BufReader::new(file);
339                let mut enabled = vec![];
340                for line in reader.lines() {
341                    let line = line?;
342                    if line.trim().is_empty() || line.trim().starts_with('#') {
343                        continue;
344                    }
345                    let Some((component, val)) = line.split_once('=') else {
346                        println!("invalid config line {}, discarded", line);
347                        continue;
348                    };
349                    if component == "RISEDEV_CONFIGURED" {
350                        continue;
351                    }
352                    match Components::from_env(component) {
353                        Some(component) => {
354                            if val == "true" {
355                                enabled.push(component);
356                            }
357                        }
358                        None => {
359                            println!("unknown configure {}, discarded", component);
360                            continue;
361                        }
362                    }
363                }
364                enabled
365            }
366            _ => {
367                println!(
368                    "RiseDev component config not found, generating {}",
369                    file_path
370                );
371                Components::default_enabled().to_vec()
372            }
373        }
374    };
375
376    let chosen = match &opts.command {
377        Some(Commands::Default) => {
378            println!("Using default config");
379            Components::default_enabled().to_vec()
380        }
381        Some(Commands::Enable { component }) => {
382            let mut chosen = chosen;
383            chosen.push(*component);
384            chosen
385        }
386        Some(Commands::Disable { component }) => {
387            chosen.into_iter().filter(|x| x != component).collect()
388        }
389        None => match configure(&chosen)? {
390            Some(chosen) => chosen,
391            None => {
392                println!("Quit without saving");
393                println!("=========================");
394                return Ok(());
395            }
396        },
397    };
398
399    println!("=== Enabled Components ===");
400    for component in all::<Components>() {
401        println!(
402            "{}: {}",
403            component.title(),
404            if chosen.contains(&component) {
405                style("enabled").green()
406            } else {
407                style("disabled").dim()
408            }
409        );
410    }
411
412    println!("Configuration saved at {}", file_path);
413    println!("=========================");
414
415    let mut file = BufWriter::new(
416        OpenOptions::new()
417            .write(true)
418            .truncate(true)
419            .create(true)
420            .open(&file_path)
421            .context(format!("failed to open component config at {}", file_path))?,
422    );
423
424    writeln!(file, "RISEDEV_CONFIGURED=true")?;
425    writeln!(file)?;
426
427    for component in all::<Components>() {
428        writeln!(file, "# {}", component.title())?;
429        writeln!(
430            file,
431            "# {}",
432            component.description().trim().split('\n').join("\n# ")
433        )?;
434        if chosen.contains(&component) {
435            writeln!(file, "{}=true", component.env())?;
436        } else {
437            writeln!(file, "# {}=true", component.env())?;
438        }
439        writeln!(file)?;
440    }
441
442    file.flush()?;
443
444    println!(
445        "RiseDev will {} the components you've enabled.",
446        style("only download").bold()
447    );
448    println!(
449        "If you want to use these components, please {} in {} to start that component.",
450        style("modify the cluster config").yellow().bold(),
451        style("risedev.yml").bold(),
452    );
453    println!("See CONTRIBUTING.md or RiseDev's readme for more information.");
454
455    Ok(())
456}