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 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}