hyperlane-quick-start 23.0.46

A lightweight rust http server with middleware, websocket, sse, and tcp support, built on tokio for cross-platform async networking, hyperlane simplifies modern web service development.
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
//! Builds the frontend packages of the server before the server itself is built.
//!
//! The frontend lives in its own workspace under `frontend/`: every page is
//! a package of that workspace, and every package is a WebAssembly
//! application of the euv framework. This build script walks the packages,
//! builds each of them with the euv command line interface and writes the
//! artifacts into the static resource directory the server serves.
//!
//! The build is skipped when the frontend workspace is absent, which is the
//! case for the published crate, and when `EUV_FRONTEND_SKIP` is set, which
//! is the case for the pipeline jobs that do not ship the browser assets.

mod frontend_build;

use std::{
    env::var,
    fs::{self, File, create_dir_all, read_to_string, write},
    path::{Path, PathBuf},
    process::{Command, Stdio},
    time::SystemTime,
};

use crate::frontend_build::*;

/// Entry point of the build script.
///
/// Builds every page package of the frontend workspace and writes the
/// artifacts into the static resource directory.
///
/// # Returns
///
/// - `Result<(), Box<dyn std::error::Error>>` - The frontend is built, the
///   build was skipped, or the build failed.
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let manifest_dir: PathBuf = PathBuf::from(var(ENV_MANIFEST_DIR)?);
    let frontend_dir: PathBuf = manifest_dir.join(FRONTEND_DIR);
    println!(
        "cargo:rerun-if-changed={}",
        frontend_dir.join(PAGES_DIR).display()
    );
    // Every page carries the shared package, so a change of the shared package
    // changes every page. Without this the build script does not run again
    // and the pages keep the code of the shared package they were built with.
    println!(
        "cargo:rerun-if-changed={}",
        frontend_dir.join(SHARED_DIR).display()
    );
    println!(
        "cargo:rerun-if-changed={}",
        frontend_dir.join("Cargo.toml").display()
    );
    println!("cargo:rerun-if-env-changed={ENV_SKIP}");
    println!("cargo:rerun-if-env-changed={ENV_CLI}");
    if var(ENV_SKIP).is_ok() {
        println!("cargo:warning=the frontend build is skipped by {ENV_SKIP}");
        return Ok(());
    }
    if !frontend_dir.join(PAGES_DIR).is_dir() {
        println!("cargo:warning=the frontend workspace is absent, no page is built");
        return Ok(());
    }
    let release: bool = var(ENV_PROFILE).unwrap_or_default() == PROFILE_RELEASE;
    let packages: Vec<PathBuf> = page_packages(&frontend_dir);
    if packages.is_empty() {
        println!("cargo:warning=the frontend workspace holds no page package");
        return Ok(());
    }
    for package in packages {
        build_page(&manifest_dir, &package, release)?;
    }
    Ok(())
}

/// Collects the page packages of the frontend workspace.
///
/// # Arguments
///
/// - `&Path` - The frontend workspace.
///
/// # Returns
///
/// - `Vec<PathBuf>` - The package directory of every page.
fn page_packages(frontend_dir: &Path) -> Vec<PathBuf> {
    let pages_dir: PathBuf = frontend_dir.join(PAGES_DIR);
    let Ok(entries) = fs::read_dir(&pages_dir) else {
        return Vec::new();
    };
    let mut packages: Vec<PathBuf> = entries
        .filter_map(|entry: std::io::Result<fs::DirEntry>| entry.ok())
        .map(|entry: fs::DirEntry| entry.path())
        .filter(|path: &PathBuf| path.join(MANIFEST_NAME).is_file())
        .collect();
    packages.sort();
    packages
}

/// Builds one page package and writes its artifacts into the static
/// resource directory.
///
/// The package is skipped when its artifacts are newer than its sources and
/// were built with the same profile, so a rebuild of the server alone does
/// not rebuild the browser assets.
///
/// # Arguments
///
/// - `&Path` - The directory of the server package.
/// - `&Path` - The package directory of the page.
/// - `bool` - Whether the page is built with the release profile.
///
/// # Returns
///
/// - `Result<(), Box<dyn std::error::Error>>` - The page is built, or the
///   build failed.
fn build_page(
    manifest_dir: &Path,
    package: &Path,
    release: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    let name: String = match package
        .file_name()
        .and_then(|name: &std::ffi::OsStr| name.to_str())
    {
        Some(name) => name.to_string(),
        None => return Ok(()),
    };
    let output_dir: PathBuf = manifest_dir.join(STATIC_DIR).join(&name);
    let mode: &str = if release {
        PROFILE_RELEASE
    } else {
        PROFILE_DEV
    };
    let shared_dir: PathBuf = manifest_dir.join(FRONTEND_DIR).join(SHARED_DIR);
    if is_fresh(package, &shared_dir, &output_dir, mode) {
        println!("cargo:warning=the page {name} is up to date");
        return Ok(());
    }
    let mut command: Command = Command::new(cli_binary());
    command.arg(CLI_BUILD);
    command.arg(ARG_CRATE_PATH).arg(package);
    command
        .arg(ARG_WWW_DIR)
        .arg(relative_output_dir(manifest_dir, package, &output_dir));
    if release {
        command.arg(ARG_RELEASE);
    } else {
        command.arg(ARG_DEV);
    }
    command.current_dir(package);
    command.env(ENV_TARGET_DIR, frontend_target_dir(manifest_dir));
    command.env_remove(ENV_ENCODED_RUSTFLAGS);
    command.env_remove(ENV_RUSTFLAGS);
    redirect_output(&mut command, &name);
    let status: std::process::ExitStatus = command.status().map_err(|_| MESSAGE_NO_CLI)?;
    if !status.success() {
        report_build_log(&name);
        return Err(format!("the page {name} could not be built by the {CLI_NAME} command").into());
    }
    check_cli_version(&name)?;
    write_stamp(&name, mode);
    println!(
        "cargo:warning=the page {name} was built into {}",
        output_dir.display()
    );
    Ok(())
}

/// Prints the tail of the log of a page that could not be built.
///
/// The output of the command line interface goes into the log of the page,
/// so a build that fails without a reason is reported with the last lines
/// of that log.
///
/// # Arguments
///
/// - `&str` - The name of the page that could not be built.
///
/// # Returns
///
/// - `()` - The tail of the log is printed.
fn report_build_log(name: &str) {
    let log_file: PathBuf = log_path(name);
    let Ok(log) = fs::read_to_string(&log_file) else {
        return;
    };
    let lines: Vec<&str> = log.lines().collect();
    let start: usize = lines.len().saturating_sub(LOG_TAIL_LINES);
    for line in &lines[start..] {
        println!("cargo:warning={name}: {line}");
    }
}

/// Checks that the command line interface that built a page is the version
/// the project expects.
///
/// The command line interface writes the path of its own sources into its
/// log, and that path carries the version of the crate it was built from.
/// A build with another version would produce artifacts that the pipeline
/// and the images of the server do not produce, so the build stops and says
/// which binary to point at.
///
/// # Arguments
///
/// - `&str` - The name of the page that was built.
///
/// # Returns
///
/// - `Result<(), Box<dyn std::error::Error>>` - The version of the command
///   line interface is the expected one, or the build stops.
fn check_cli_version(name: &str) -> Result<(), Box<dyn std::error::Error>> {
    let log_file: PathBuf = log_path(name);
    let Ok(log) = fs::read_to_string(&log_file) else {
        return Ok(());
    };
    let mut versions: Vec<String> = Vec::new();
    let mut rest: &str = log.as_str();
    while let Some(index) = rest.find(CLI_VERSION_PATTERN) {
        rest = &rest[index + CLI_VERSION_PATTERN.len()..];
        let end: usize = rest
            .find(|c: char| !c.is_ascii_digit() && c != '.')
            .unwrap_or(rest.len());
        let version: String = rest[..end].to_string();
        if !version.is_empty() && !versions.contains(&version) {
            versions.push(version);
        }
    }
    for version in versions {
        if version != CLI_VERSION {
            return Err(MESSAGE_CLI_VERSION.into());
        }
    }
    Ok(())
}

/// Returns the target directory the frontend workspace builds into.
///
/// The build script of the server and the build of a page would otherwise
/// share one target directory, and the page build would wait for the lock
/// the server build holds: the server build waits for the page build, and
/// neither of them finishes.
///
/// # Arguments
///
/// - `&Path` - The directory of the server package.
///
/// # Returns
///
/// - `PathBuf` - The target directory of the frontend workspace.
fn frontend_target_dir(manifest_dir: &Path) -> PathBuf {
    manifest_dir.join(FRONTEND_DIR).join(TARGET_DIR)
}

/// Sends the output of the command line interface into the log file.
///
/// The build script writes into a pipe that nobody reads while the script
/// runs, and a build that writes more into it than the pipe holds would
/// block forever. The log file keeps the output of the build readable.
///
/// # Arguments
///
/// - `&mut Command` - The command the output is redirected away from.
/// - `&str` - The name of the page that is built.
fn redirect_output(command: &mut Command, name: &str) {
    let log_file: PathBuf = log_path(name);
    if let Some(parent) = log_file.parent() {
        let created: std::io::Result<()> = create_dir_all(parent);
        let _: std::io::Result<()> = created;
    }
    match File::create(log_file) {
        Ok(log) => match log.try_clone() {
            Ok(errors) => {
                command.stdout(Stdio::from(log));
                command.stderr(Stdio::from(errors));
            }
            Err(_) => {
                command.stdout(Stdio::null());
                command.stderr(Stdio::null());
            }
        },
        Err(_) => {
            command.stdout(Stdio::null());
            command.stderr(Stdio::null());
        }
    }
}

/// Returns the path of the log the frontend build writes to.
///
/// # Arguments
///
/// - `&str` - The name of the page that is built.
///
/// # Returns
///
/// - `PathBuf` - The path of the log.
fn log_path(name: &str) -> PathBuf {
    let out_dir: PathBuf = PathBuf::from(var(ENV_OUT_DIR).unwrap_or_default());
    out_dir.join(STAMP_DIR).join(name).join(LOG_NAME)
}

/// Returns the command line interface binary that builds the pages.
///
/// The binary is taken from the `EUV_CLI` environment variable when it is
/// set, so a machine that keeps the tool outside its path can point the
/// build at it.
///
/// # Returns
///
/// - `String` - The path of the binary.
fn cli_binary() -> String {
    var(ENV_CLI).unwrap_or_else(|_| CLI_NAME.to_string())
}

/// Reports whether the artifacts of a page are newer than its sources.
///
/// The sources of the shared crate are part of every page, so a change in
/// them rebuilds every page as well.
///
/// # Arguments
///
/// - `&Path` - The package directory of the page.
/// - `&Path` - The directory of the shared package.
/// - `&Path` - The directory the artifacts were written to.
/// - `&str` - The profile the artifacts were built with.
///
/// # Returns
///
/// - `bool` - Whether the page can be left alone.
fn is_fresh(package: &Path, shared: &Path, output_dir: &Path, mode: &str) -> bool {
    let index: PathBuf = output_dir.join(INDEX_NAME);
    let Ok(built_at) = index
        .metadata()
        .and_then(|meta: fs::Metadata| meta.modified())
    else {
        return false;
    };
    if stamp_mode(package).as_deref() != Some(mode) {
        return false;
    }
    let page_at: Option<SystemTime> = newest_source_time(package);
    let shared_at: Option<SystemTime> = newest_source_time(shared);
    match (page_at, shared_at) {
        (Some(page_at), Some(shared_at)) => page_at.max(shared_at) <= built_at,
        (Some(page_at), None) => page_at <= built_at,
        (None, Some(shared_at)) => shared_at <= built_at,
        (None, None) => false,
    }
}

/// Reads the profile the artifacts of a page were built with.
///
/// # Arguments
///
/// - `&Path` - The package directory of the page.
///
/// # Returns
///
/// - `Option<String>` - The profile, absent when the page was never built.
fn stamp_mode(package: &Path) -> Option<String> {
    read_to_string(stamp_path(package)).ok()
}

/// Records the profile the artifacts of a page were built with.
///
/// # Arguments
///
/// - `&str` - The name of the page.
/// - `&str` - The profile the page was built with.
///
/// # Returns
///
/// - `()` - The stamp is written, or the write failed and is ignored.
fn write_stamp(name: &str, mode: &str) {
    let stamp: PathBuf = stamp_path(Path::new(name));
    if let Some(parent) = stamp.parent()
        && create_dir_all(parent).is_ok()
    {
        let written: std::io::Result<()> = write(stamp, mode);
        let _: std::io::Result<()> = written;
    }
}

/// Returns the path of the stamp of a page.
///
/// # Arguments
///
/// - `&Path` - The package directory of the page.
///
/// # Returns
///
/// - `PathBuf` - The path of the stamp.
fn stamp_path(package: &Path) -> PathBuf {
    let name: String = package
        .file_name()
        .and_then(|name: &std::ffi::OsStr| name.to_str())
        .unwrap_or_default()
        .to_string();
    let out_dir: PathBuf = PathBuf::from(var(ENV_OUT_DIR).unwrap_or_default());
    out_dir.join(STAMP_DIR).join(format!("{name}.stamp"))
}

/// Returns the moment the newest source of a package was written.
///
/// # Arguments
///
/// - `&Path` - The package directory of the page.
///
/// # Returns
///
/// - `Option<SystemTime>` - The moment, absent when the package holds no source.
fn newest_source_time(package: &Path) -> Option<SystemTime> {
    let mut newest: Option<SystemTime> = None;
    let mut stack: Vec<PathBuf> = vec![package.to_path_buf()];
    while let Some(directory) = stack.pop() {
        let Ok(entries) = fs::read_dir(&directory) else {
            continue;
        };
        for entry in entries.filter_map(|entry: std::io::Result<fs::DirEntry>| entry.ok()) {
            let path: PathBuf = entry.path();
            let Ok(metadata) = fs::metadata(&path) else {
                continue;
            };
            if metadata.is_dir() {
                if path
                    .file_name()
                    .and_then(|name: &std::ffi::OsStr| name.to_str())
                    == Some("target")
                {
                    continue;
                }
                stack.push(path);
                continue;
            }
            let Ok(modified) = metadata.modified() else {
                continue;
            };
            if newest.is_none_or(|current: SystemTime| modified > current) {
                newest = Some(modified);
            }
        }
    }
    newest
}

/// Returns the output directory of a page relative to its package.
///
/// The command line interface resolves the directory against the package it
/// builds, so the path has to be relative to it.
///
/// # Arguments
///
/// - `&Path` - The directory of the server package.
/// - `&Path` - The package directory of the page.
/// - `&Path` - The directory the artifacts are written to.
///
/// # Returns
///
/// - `String` - The relative directory.
fn relative_output_dir(manifest_dir: &Path, package: &Path, output_dir: &Path) -> String {
    let depth: usize = package
        .strip_prefix(manifest_dir)
        .map(|path: &Path| path.components().count())
        .unwrap_or_default();
    let mut relative: String = String::new();
    for _ in 0..depth {
        relative.push_str("../");
    }
    let tail: String = output_dir
        .strip_prefix(manifest_dir)
        .map(|path: &Path| path.to_string_lossy().replace('\\', "/"))
        .unwrap_or_default();
    relative.push_str(&tail);
    relative
}