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