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