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