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