1use std::io::{BufRead, BufReader, BufWriter, Write};
16
17use anyhow::{Context, Result};
18use clap::{Parser, Subcommand, ValueEnum};
19use console::style;
20use dialoguer::MultiSelect;
21use enum_iterator::{Sequence, all};
22use fs_err::OpenOptions;
23use itertools::Itertools;
24
25#[derive(Parser)]
26#[clap(author, version, about, long_about = None)]
27#[clap(propagate_version = true)]
28#[clap(infer_subcommands = true)]
29pub struct RiseDevConfigOpts {
30 #[clap(subcommand)]
31 command: Option<Commands>,
32 #[clap(short, long)]
33 file: String,
34}
35
36#[derive(Subcommand)]
37#[clap(infer_subcommands = true)]
38enum Commands {
39 Enable {
41 #[clap(value_enum)]
43 component: Components,
44 },
45 Disable {
47 #[clap(value_enum)]
49 component: Components,
50 },
51 Default,
53}
54
55#[expect(clippy::enum_variant_names)]
56#[derive(Clone, Copy, Debug, Sequence, PartialEq, Eq, ValueEnum)]
57pub enum Components {
58 #[clap(name = "minio")]
59 Minio,
60 Lakekeeper,
61 Hdfs,
62 PrometheusAndGrafana,
63 Pubsub,
64 Redis,
65 Tracing,
66 RustComponents,
67 UseSystem,
68 BuildConnectorNode,
69 Dashboard,
70 Release,
71 Sanitizer,
72 DynamicLinking,
73 HummockTrace,
74 Coredump,
75 NoBacktrace,
76 Udf,
77 NoDefaultFeatures,
78 Moat,
79 DataFusion,
80 Adbc,
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 Self::DataFusion => "[Build] Enable DataFusion",
107 Self::Adbc => "[Component] ADBC Snowflake Driver",
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 Self::Adbc => {
231 "
232Enable ADBC (Arrow Database Connectivity) Snowflake driver support.
233Required if you want to use ADBC Snowflake source.
234This will download the ADBC Snowflake driver shared library (.so/.dylib)."
235 }
236 }
237 .into()
238 }
239
240 pub fn from_env(env: impl AsRef<str>) -> Option<Self> {
241 match env.as_ref() {
242 "ENABLE_MINIO" => Some(Self::Minio),
243 "ENABLE_LAKEKEEPER" => Some(Self::Lakekeeper),
244 "ENABLE_HDFS" => Some(Self::Hdfs),
245 "ENABLE_PROMETHEUS_GRAFANA" => Some(Self::PrometheusAndGrafana),
246 "ENABLE_PUBSUB" => Some(Self::Pubsub),
247 "ENABLE_BUILD_RUST" => Some(Self::RustComponents),
248 "USE_SYSTEM_RISINGWAVE" => Some(Self::UseSystem),
249 "ENABLE_BUILD_DASHBOARD" => Some(Self::Dashboard),
250 "ENABLE_COMPUTE_TRACING" => Some(Self::Tracing),
251 "ENABLE_RELEASE_PROFILE" => Some(Self::Release),
252 "ENABLE_DYNAMIC_LINKING" => Some(Self::DynamicLinking),
253 "ENABLE_SANITIZER" => Some(Self::Sanitizer),
254 "ENABLE_REDIS" => Some(Self::Redis),
255 "ENABLE_BUILD_RW_CONNECTOR" => Some(Self::BuildConnectorNode),
256 "ENABLE_HUMMOCK_TRACE" => Some(Self::HummockTrace),
257 "ENABLE_COREDUMP" => Some(Self::Coredump),
258 "DISABLE_BACKTRACE" => Some(Self::NoBacktrace),
259 "ENABLE_UDF" => Some(Self::Udf),
260 "DISABLE_DEFAULT_FEATURES" => Some(Self::NoDefaultFeatures),
261 "ENABLE_MOAT" => Some(Self::Moat),
262 "ENABLE_DATAFUSION" => Some(Self::DataFusion),
263 "ENABLE_ADBC" => Some(Self::Adbc),
264 _ => None,
265 }
266 }
267
268 pub fn env(&self) -> String {
269 match self {
270 Self::Minio => "ENABLE_MINIO",
271 Self::Lakekeeper => "ENABLE_LAKEKEEPER",
272 Self::Hdfs => "ENABLE_HDFS",
273 Self::PrometheusAndGrafana => "ENABLE_PROMETHEUS_GRAFANA",
274 Self::Pubsub => "ENABLE_PUBSUB",
275 Self::Redis => "ENABLE_REDIS",
276 Self::RustComponents => "ENABLE_BUILD_RUST",
277 Self::UseSystem => "USE_SYSTEM_RISINGWAVE",
278 Self::Dashboard => "ENABLE_BUILD_DASHBOARD",
279 Self::Tracing => "ENABLE_COMPUTE_TRACING",
280 Self::Release => "ENABLE_RELEASE_PROFILE",
281 Self::Sanitizer => "ENABLE_SANITIZER",
282 Self::BuildConnectorNode => "ENABLE_BUILD_RW_CONNECTOR",
283 Self::DynamicLinking => "ENABLE_DYNAMIC_LINKING",
284 Self::HummockTrace => "ENABLE_HUMMOCK_TRACE",
285 Self::Coredump => "ENABLE_COREDUMP",
286 Self::NoBacktrace => "DISABLE_BACKTRACE",
287 Self::Udf => "ENABLE_UDF",
288 Self::NoDefaultFeatures => "DISABLE_DEFAULT_FEATURES",
289 Self::Moat => "ENABLE_MOAT",
290 Self::DataFusion => "ENABLE_DATAFUSION",
291 Self::Adbc => "ENABLE_ADBC",
292 }
293 .into()
294 }
295
296 pub fn default_enabled() -> &'static [Self] {
297 &[Self::RustComponents]
298 }
299}
300
301fn configure(chosen: &[Components]) -> Result<Option<Vec<Components>>> {
302 println!("=== Configure RiseDev ===");
303
304 let all_components = all::<Components>().collect_vec();
305
306 const ITEMS_PER_PAGE: usize = 6;
307
308 let items = all_components
309 .iter()
310 .map(|c| {
311 let title = c.title();
312 let desc = style(
313 ("\n".to_owned() + c.description().trim())
314 .split('\n')
315 .join("\n "),
316 )
317 .dim();
318
319 (format!("{title}{desc}",), chosen.contains(c))
320 })
321 .collect_vec();
322
323 let Some(chosen_indices) = MultiSelect::new()
324 .with_prompt(
325 format!(
326 "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 {}",
327 style("↑ / ↓ / ← / → ").reverse(),
328 style("Enter").reverse(),
329 style("Esc / q").reverse(),
330 style("Space").reverse(),
331 )
332 )
333 .items_checked(items)
334 .max_length(ITEMS_PER_PAGE)
335 .interact_opt()? else {
336 return Ok(None);
337 };
338
339 let chosen = chosen_indices
340 .into_iter()
341 .map(|i| all_components[i])
342 .collect_vec();
343
344 Ok(Some(chosen))
345}
346
347fn main() -> Result<()> {
348 let opts = RiseDevConfigOpts::parse();
349 let file_path = opts.file;
350
351 let chosen = {
352 match OpenOptions::new().read(true).open(&file_path) {
353 Ok(file) => {
354 let reader = BufReader::new(file);
355 let mut enabled = vec![];
356 for line in reader.lines() {
357 let line = line?;
358 if line.trim().is_empty() || line.trim().starts_with('#') {
359 continue;
360 }
361 let Some((component, val)) = line.split_once('=') else {
362 println!("invalid config line {}, discarded", line);
363 continue;
364 };
365 if component == "RISEDEV_CONFIGURED" {
366 continue;
367 }
368 match Components::from_env(component) {
369 Some(component) => {
370 if val == "true" {
371 enabled.push(component);
372 }
373 }
374 None => {
375 println!("unknown configure {}, discarded", component);
376 continue;
377 }
378 }
379 }
380 enabled
381 }
382 _ => {
383 println!(
384 "RiseDev component config not found, generating {}",
385 file_path
386 );
387 Components::default_enabled().to_vec()
388 }
389 }
390 };
391
392 let chosen = match &opts.command {
393 Some(Commands::Default) => {
394 println!("Using default config");
395 Components::default_enabled().to_vec()
396 }
397 Some(Commands::Enable { component }) => {
398 let mut chosen = chosen;
399 chosen.push(*component);
400 chosen
401 }
402 Some(Commands::Disable { component }) => {
403 chosen.into_iter().filter(|x| x != component).collect()
404 }
405 None => match configure(&chosen)? {
406 Some(chosen) => chosen,
407 None => {
408 println!("Quit without saving");
409 println!("=========================");
410 return Ok(());
411 }
412 },
413 };
414
415 println!("=== Enabled Components ===");
416 for component in all::<Components>() {
417 println!(
418 "{}: {}",
419 component.title(),
420 if chosen.contains(&component) {
421 style("enabled").green()
422 } else {
423 style("disabled").dim()
424 }
425 );
426 }
427
428 println!("Configuration saved at {}", file_path);
429 println!("=========================");
430
431 let mut file = BufWriter::new(
432 OpenOptions::new()
433 .write(true)
434 .truncate(true)
435 .create(true)
436 .open(&file_path)
437 .context(format!("failed to open component config at {}", file_path))?,
438 );
439
440 writeln!(file, "RISEDEV_CONFIGURED=true")?;
441 writeln!(file)?;
442
443 for component in all::<Components>() {
444 writeln!(file, "# {}", component.title())?;
445 writeln!(
446 file,
447 "# {}",
448 component.description().trim().split('\n').join("\n# ")
449 )?;
450 if chosen.contains(&component) {
451 writeln!(file, "{}=true", component.env())?;
452 } else {
453 writeln!(file, "# {}=true", component.env())?;
454 }
455 writeln!(file)?;
456 }
457
458 file.flush()?;
459
460 println!(
461 "RiseDev will {} the components you've enabled.",
462 style("only download").bold()
463 );
464 println!(
465 "If you want to use these components, please {} in {} to start that component.",
466 style("modify the cluster config").yellow().bold(),
467 style("risedev.yml").bold(),
468 );
469 println!("See CONTRIBUTING.md or RiseDev's readme for more information.");
470
471 Ok(())
472}