-
Notifications
You must be signed in to change notification settings - Fork 12
/
main.rs
502 lines (472 loc) · 16.5 KB
/
main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
// Copyright (C) 2023 Bryan A. Jones.
//
// This file is part of the CodeChat Editor. The CodeChat Editor is free
// software: you can redistribute it and/or modify it under the terms of the GNU
// General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// The CodeChat Editor is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// the CodeChat Editor. If not, see
// [http://www.gnu.org/licenses](http://www.gnu.org/licenses).
//
// # `main.rs` -- Entrypoint for the CodeChat Editor Builder
//
// This code uses [dist](https://opensource.axo.dev/cargo-dist/book/) as a part
// of the release process. To update the `./release.yaml` file this tool
// creates:
//
// 1. Edit `server/dist-workspace.toml`: change `allow-dirty` to `[]`.
// 2. Run `dist init` and accept the defaults, then run `dist generate`.
// 3. Review changes to `./release.yaml`, reapplying hand edits.
// 4. Revert the changes to `server/dist-workspace.toml`.
// 5. Test
//
// ## Imports
//
// ### Standard library
use std::{ffi::OsStr, fs, path::Path, process::Command};
// ### Third-party
use clap::{Parser, Subcommand};
use cmd_lib::run_cmd;
use current_platform::CURRENT_PLATFORM;
use regex::Regex;
// ### Local
//
// None
//
// ## Data structures
//
// The following defines the command-line interface for the CodeChat Editor.
#[derive(Parser)]
#[command(name = "The CodeChat Editor Server", version, about, long_about=None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Install all dependencies.
Install {
/// True to install developer-only dependencies.
#[arg(short, long, default_value_t = false)]
dev: bool,
},
/// Update all dependencies.
Update,
/// Run lints and tests.
Test,
/// Build everything.
Build,
/// Change the version for the client, server, and extensions.
ChangeVersion {
/// The new version number, such as "0.1.1".
new_version: String,
},
/// Steps to run before `cargo dist build`.
Prerelease,
/// Steps to run after `cargo dist build`. This builds and publishes a
/// VSCode release.
Postrelease {
/// Receives a target triple, such as `x86_64-pc-windows-msvc`. We can't
/// always infer this, since `dist` cross-compiles the server on OS X,
/// while this program isn't cross-compiled.
#[arg(short, long, default_value_t = CURRENT_PLATFORM.to_string())]
target: String,
/// The CI build passes this. We don't use it, but must receive it to
/// avoid an error.
#[arg(short, long)]
artifacts: Option<String>,
},
}
// ## Code
//
// ### Utilities
//
// These functions are called by the build support functions.
/// On Windows, scripts must be run from a shell; on Linux and OS X, scripts are
/// directly executable. This function runs a script regardless of OS.
fn run_script<T: AsRef<OsStr>, P: AsRef<Path> + std::fmt::Display>(
// The script to run.
script: T,
// Arguments to pass.
args: &[T],
// The directory to run the script in.
dir: P,
// True to report errors based on the process' exit code; false to ignore
// the code.
check_exit_code: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let mut process;
if cfg!(windows) {
process = Command::new("cmd");
process.arg("/c").arg(script);
} else {
process = Command::new(script);
};
process.args(args).current_dir(&dir);
// A bit crude, but displays the command being run.
println!("{dir}: {process:#?}");
let exit_code = process.status()?.code();
if exit_code == Some(0) || (exit_code.is_some() && !check_exit_code) {
Ok(())
} else {
Err("npm exit code indicates failure".into())
}
}
/// Quickly synchronize the `src` directory with the `dest` directory, by
/// copying files and removing anything in `dest` not in `src`. It uses OS
/// programs (`robocopy`/`rsync`) to accomplish this. Very important: the `src`
/// **must** end with a `/`, otherwise the Windows and Linux copies aren't
/// identical.
fn quick_copy_dir<P: AsRef<OsStr>>(
src: P,
dest: P,
files: Option<P>,
) -> Result<(), Box<dyn std::error::Error>> {
assert!(src.as_ref().to_string_lossy().ends_with('/'));
let mut copy_process;
#[cfg(windows)]
{
// From `robocopy /?`:
//
// /MIR MIRror a directory tree (equivalent to /E plus /PURGE).
//
// /MT Do multi-threaded copies with n threads (default 8).
//
// /NFL No File List - don't log file names.
//
// /NDL : No Directory List - don't log directory names.
//
// /NJH : No Job Header.
//
// /NJS : No Job Summary.
//
// /NP : No Progress - don't display percentage copied.
//
// /NS : No Size - don't log file sizes.
//
// /NC : No Class - don't log file classes.
copy_process = Command::new("robocopy");
copy_process
.args([
"/MIR", "/MT", "/NFL", "/NDL", "/NJH", "/NJS", "/NP", "/NS", "/NC",
])
.arg(&src)
.arg(&dest);
// Robocopy expects the files to copy after the dest.
if let Some(files_) = &files {
copy_process.arg(files_);
}
}
#[cfg(not(windows))]
{
// Create the dest directory, since old CI OSes don't support
// `rsync --mkpath`.
run_script(
"mkdir",
&["-p", dest.as_ref().to_str().unwrap()],
"./",
true,
)?;
let mut tmp;
let src_combined = match files.as_ref() {
Some(files_) => {
tmp = src.as_ref().to_os_string();
tmp.push(files_);
tmp.as_os_str()
}
None => src.as_ref(),
};
// Use bash to perform globbing, since rsync doesn't do this.
copy_process = Command::new("bash");
copy_process.args([
"-c",
format!(
"rsync --archive --delete {} {}",
&src_combined.to_str().unwrap(),
&dest.as_ref().to_str().unwrap()
)
.as_str(),
]);
}
// Print the command, in case this produces and error or takes a while.
println!("{:#?}", ©_process);
// Check for errors.
let exit_status = copy_process
.status()
.map_err(|err| -> String { format!("Error running copy process: {err}") })?;
let exit_code = exit_status
.code()
.expect("Copy process terminated by signal");
// Per
// [these docs](https://learn.microsoft.com/en-us/troubleshoot/windows-server/backup-and-storage/return-codes-used-robocopy-utility),
// check the return code.
if cfg!(windows) && exit_code >= 8 || !cfg!(windows) && exit_code != 0 {
Err(format!("Copy process return code {exit_code} indicates failure.").into())
} else {
Ok(())
}
}
fn remove_dir_all_if_exists<P: AsRef<Path> + std::fmt::Display>(
path: P,
) -> Result<(), Box<dyn std::error::Error>> {
if Path::new(path.as_ref()).try_exists().unwrap() {
if let Err(err) = fs::remove_dir_all(path.as_ref()) {
return Err(format!("Error removing directory tree {path}: {err}").into());
}
}
Ok(())
}
fn search_and_replace_file<
P: AsRef<Path> + std::fmt::Display,
S1: AsRef<str> + std::fmt::Display,
S2: AsRef<str>,
>(
path: P,
search_regex: S1,
replace_string: S2,
) -> Result<(), Box<dyn std::error::Error>> {
let file_contents = fs::read_to_string(&path)
.map_err(|err| -> String { format!("Unable to open file {path} for reading: {err}") })?;
let re = Regex::new(search_regex.as_ref())
.map_err(|err| -> String { format!("Error in search regex {search_regex}: {err}") })?;
let file_contents_replaced = re.replace(&file_contents, replace_string.as_ref());
assert_ne!(
file_contents, file_contents_replaced,
"No replacements made."
);
fs::write(&path, file_contents_replaced.as_bytes())
.map_err(|err| -> String { format!("Error writing to {path}: {err}") })?;
Ok(())
}
// ## Core routines
//
// These functions simplify common build-focused development tasks and support
// CI builds.
/// After updating files in the client's Node files, perform some fix-ups.
fn patch_client_npm() -> Result<(), Box<dyn std::error::Error>> {
// Apply a the fixes described in
// [issue 27](https://github.com/bjones1/CodeChat_Editor/issues/27).
//
// Insert this line...
let patch = "
selectionNotFocus = this.view.state.facet(editable) ? focused : hasSelection(this.dom, this.view.observer.selectionRange)";
// After this line.
let before_path = " let selectionNotFocus = !focused && !(this.view.state.facet(editable) || this.dom.tabIndex > -1) &&
hasSelection(this.dom, this.view.observer.selectionRange) && !(activeElt && this.dom.contains(activeElt));";
// First, see if the patch was applied already.
let index_js_path = Path::new("../client/node_modules/@codemirror/view/dist/index.js");
let index_js = fs::read_to_string(index_js_path)?;
if !index_js.contains(patch) {
let patch_loc = index_js
.find(before_path)
.expect("Patch location not found.")
+ before_path.len();
let patched_index_js = format!(
"{}{patch}{}",
&index_js[..patch_loc],
&index_js[patch_loc..]
);
fs::write(index_js_path, &patched_index_js)?;
}
// Copy across the parts of MathJax that are needed, since bundling it is
// difficult.
quick_copy_dir(
"../client/node_modules/mathjax/",
"../client/static/mathjax",
None,
)?;
quick_copy_dir(
"../client/node_modules/mathjax-modern-font/chtml/",
"../client/static/mathjax-modern-font/chtml",
None,
)?;
// Copy over the graphviz files needed.
quick_copy_dir(
"../client/node_modules/graphviz-webcomponent/dist/",
"../client/static/graphviz-webcomponent",
Some("renderer.min.js*"),
)?;
Ok(())
}
fn run_install(dev: bool) -> Result<(), Box<dyn std::error::Error>> {
run_script("npm", &["install"], "../client", true)?;
patch_client_npm()?;
run_script("npm", &["install"], "../extensions/VSCode", true)?;
run_cmd!(
cargo fetch --manifest-path=../builder/Cargo.toml;
cargo fetch;
)?;
if dev {
// If the dist install reports an error, perhaps it's already installed.
if run_cmd!(cargo install --locked cargo-dist;).is_err() {
run_cmd!(dist --version;)?;
}
run_cmd!(
cargo install --locked cargo-outdated;
cargo install cargo-sort;
)?;
}
Ok(())
}
fn run_update() -> Result<(), Box<dyn std::error::Error>> {
run_script("npm", &["update"], "../client", true)?;
patch_client_npm()?;
run_script("npm", &["update"], "../extensions/VSCode", true)?;
run_cmd!(
cargo update --manifest-path=../builder/Cargo.toml;
cargo update;
)?;
// Simply display outdated dependencies, but don't considert them an error.
run_script("npm", &["outdated"], "../client", false)?;
run_script("npm", &["outdated"], "../extensions/VSCode", false)?;
run_cmd!(
cargo outdated --manifest-path=../builder/Cargo.toml;
cargo outdated;
)?;
Ok(())
}
fn run_test() -> Result<(), Box<dyn std::error::Error>> {
// On Windows, `cargo sort --check` fails since it default to LF, not CRLF,
// line endings. Work around this by changing this setting only on Windows.
// See the
// [cargo sort config docs](https://github.com/DevinR528/cargo-sort?tab=readme-ov-file#config)
// and the
// [related issue](https://github.com/DevinR528/cargo-sort/issues/85).
//
// However, this still fails: `cargo sort` uses
// [inconsistent line endings](https://github.com/DevinR528/cargo-sort/issues/86).
/***
#[cfg(windows)]
{
fs::write("tomlfmt.toml", "crlf = true")
.map_err(|err| -> String { format!("Unable to write tomlfmt.toml: {err}") })?;
}
*/
// The `-D warnings` flag causes clippy to return a non-zero exit status if
// it issues warnings.
run_cmd!(
cargo clippy --all-targets -- -D warnings;
cargo fmt --check;
cargo clippy --all-targets --manifest-path=../builder/Cargo.toml -- -D warnings;
cargo fmt --check --manifest-path=../builder/Cargo.toml;
)?;
// `cargo sort` produces false positives under Windows. Ignore for now. See
// the above comments. It also doesn't support the
#[cfg(not(windows))]
run_cmd!(
cargo sort --check;
cd ../builder;
cargo sort --check;
)?;
run_build()?;
// Verify that compiling for release produces no errors.
run_cmd!(dist build;)?;
run_cmd!(
cargo test --manifest-path=../builder/Cargo.toml;
cargo test;
)?;
Ok(())
}
fn run_build() -> Result<(), Box<dyn std::error::Error>> {
// Clean out all bundled files before the rebuild.
remove_dir_all_if_exists("../client/static/bundled")?;
run_script("npm", &["run", "build"], "../client", true)?;
run_script("npm", &["run", "compile"], "../extensions/VSCode", true)?;
run_cmd!(
cargo build --manifest-path=../builder/Cargo.toml;
cargo build;
)?;
Ok(())
}
fn run_change_version(new_version: &String) -> Result<(), Box<dyn std::error::Error>> {
let replacement_string = format!("${{1}}{new_version}${{2}}");
search_and_replace_file(
"Cargo.toml",
r#"(\r?\nversion = ")[\d.]+("\r?\n)"#,
&replacement_string,
)?;
let json_search_regex = r#"(\r?\n "version": ")[\d.]+(",\r?\n)"#;
search_and_replace_file(
"../client/package.json",
json_search_regex,
&replacement_string,
)?;
search_and_replace_file(
"../extensions/VSCode/package.json",
json_search_regex,
&replacement_string,
)?;
Ok(())
}
fn run_prerelease() -> Result<(), Box<dyn std::error::Error>> {
// Clean out all bundled files before the rebuild.
remove_dir_all_if_exists("../client/static/bundled")?;
run_install(true)?;
run_script("npm", &["run", "dist"], "../client", true)?;
Ok(())
}
fn run_postrelease(target: &str) -> Result<(), Box<dyn std::error::Error>> {
let server_dir = "../extensions/VSCode/server";
// Only clean the `server/` directory if it exists.
remove_dir_all_if_exists(server_dir)?;
// Translate from the target triple to VSCE's target parameter.
let vsce_target = match target {
"x86_64-pc-windows-msvc" => "win32-x64",
"x86_64-unknown-linux-gnu" => "linux-x64",
"x86_64-apple-darwin" => "darwin-x64",
"aarch64-apple-darwin" => "darwin-arm64",
_ => panic!("Unsupported platform {target}."),
};
let src_name = format!("codechat-editor-server-{target}");
quick_copy_dir(
format!("target/distrib/{src_name}/").as_str(),
"../extensions/VSCode/server",
None,
)?;
run_script(
"npx",
&["vsce", "package", "--target", vsce_target],
"../extensions/VSCode",
true,
)?;
Ok(())
}
// ## CLI implementation
//
// The following code implements the command-line interface for the CodeChat
// Editor.
impl Cli {
fn run(self) -> Result<(), Box<dyn std::error::Error>> {
match &self.command {
Commands::Install { dev } => run_install(*dev),
Commands::Update => run_update(),
Commands::Test => run_test(),
Commands::Build => run_build(),
Commands::ChangeVersion { new_version } => run_change_version(new_version),
Commands::Prerelease => run_prerelease(),
Commands::Postrelease { target, .. } => run_postrelease(target),
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
cli.run()?;
Ok(())
}
#[cfg(test)]
mod test {
use super::Cli;
use clap::CommandFactory;
// This is recommended in the
// [docs](https://docs.rs/clap/latest/clap/_derive/_tutorial/chapter_4/index.html).
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}