Глава 17: Кроссплатформенная разработка в Rust

Содержание

Добро пожаловать в главу нашего курса по Rust, где мы подробно разберём кроссплатформенную разработку. Rust позиционируется как язык, который из коробки поддерживает написание надёжного и переносимого кода для множества платформ. В этой лекции мы изучим инструменты и подходы для создания приложений, которые без лишних усилий работают на Windows, Linux, macOS и других системах. Мы рассмотрим условную компиляцию, настройку кросс-компиляции, особенности операционных систем, тестирование и практические примеры. Лекция завершится упражнением, которое поможет закрепить материал.


1. Условная компиляция: #[cfg] и feature

Условная компиляция — это способ адаптировать код под разные платформы или условия без лишних runtime-проверок, это сердце кроссплатформенности в Rust. Она позволяет компилировать только нужный код в зависимости от платформы, архитектуры или пользовательских настроек.

Rust предоставляет мощный механизм условной компиляции через атрибут #[cfg]. Это позволяет включать или исключать код в зависимости от целевой платформы, конфигурации или пользовательских флагов.

Основы #[cfg]

Атрибут #[cfg] — это макрос, который Rust использует на этапе компиляции для включения или исключения кода. Если условие истинно, код компилируется; если ложно — игнорируется. Условия задаются через предопределённые переменные или пользовательские флаги.

Rust поддерживает множество предопределённых переменных, таких как:

fn main() {
    #[cfg(target_os = "linux")]
    println!("Linux приветствует вас!");

    #[cfg(target_os = "windows")]
    println!("Windows приветствует вас!");

    #[cfg(not(target_os = "macos"))]
    println!("Это точно не macOS!");
}

Полный список целей доступен в документации Rust: Rust targets.

В примере компилятор проверяет цель (target), заданную через --target, и включает только соответствующий код.

Комбинирование условий

Rust поддерживает логические операторы для сложных проверок:

#[cfg(all(target_os = "linux", target_pointer_width = "64"))]
fn linux_64bit() {
    println!("Linux 64-bit!");
}

#[cfg(any(target_os = "windows", target_os = "macos"))]
fn win_or_mac() {
    println!("Windows или macOS!");
}

Пользовательские флаги

Вы можете определить свои флаги компиляции через командную строку с --cfg или файл конфигурации.

В файле конфигурации config.toml. Файл config.toml находится в корне проекта (рядом с Cargo.toml) или в домашней директории (~/.cargo/config.toml для глобальных настроек). В разделе [build] указываются флаги через ключ rustflags, где можно передать --cfg. Rust компилятор (rustc) читает эти настройки и применяет их при сборке.

config.toml

[build]
rustflags = ["--cfg", "debug_mode"]

main.rs

#[cfg(debug_mode)]
fn debug_only() {
    println!("Режим отладки!");
}

Запуск с пользовательским флагом: cargo build --cfg debug_mode. Это полезно для экспериментальных функций или отладки.

Атрибут feature в Cargo.toml

Для библиотек и проектов Cargo использует фич-флаги, которые задаются в Cargo.toml:

[features]
default = ["core"]
core = []
windows_gui = []

Использование в коде:

#[cfg(feature = "windows_gui")]
fn gui_mode() {
    println!("GUI-режим для Windows");
}

Запуск с активацией фичи: cargo build --features windows_gui.

Нюансы

Избегайте путаницы между #[cfg] (условие на этапе компиляции) и cfg!() (возвращает bool во время выполнения). Чрезмерное использование #[cfg] делает код трудно читаемым. Для больших различий в логике между платформами рекомендуется вынести платформозависимую логику в отдельные модули: linux_impl.rs, windows_impl.rs.

  #[cfg(target_os = "linux")]
  mod linux_impl;
  #[cfg(target_os = "windows")]
  mod windows_impl;

  #[cfg(target_os = "linux")]
  use linux_impl::run;
  #[cfg(target_os = "windows")]
  use windows_impl::run;

  fn main() {
      run();
  }

2. Кросс-компиляция: настройка toolchain

Кросс-компиляция позволяет собирать бинарники для платформ, отличных от хост-системы, т.е. собирать бинарные файлы для другой платформы, отличной от той, на которой вы работаете. Например, собрать Windows-приложение на Linux.

Установка целей

Rust использует rustup для управления целями компиляции которых поддерживает множество. Установите нужные цели с помощью команд:

rustup target add x86_64-pc-windows-gnu
rustup target add x86_64-unknown-linux-gnu
rustup target add x86_64-apple-darwin

Посмотреть список доступных целей можно командой: rustup target list. Установленные цели будут отмечены как (installed).

Настройка linker’а

Для кросс-компиляции нужен linker (связыватель), соответствующий целевой платформе. Каждая цель требует подходящий linker (связыватель):

Пример файла .cargo/config.toml:

[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"

Сборка проекта для нужной цели:

cargo build --target x86_64-pc-windows-gnu
cargo build --target x86_64-unknown-linux-gnu

Атрибут windows_subsystem

Что это такое?

windows_subsystem — это внутренняя инструкция Rust (inner attribute, с #![...]), которая указывает тип подсистемы Windows в PE-файле (Portable Executable). Она определяет, как Windows будет запускать приложение: с консольным окном или без него.

Откуда взялось?

Этот атрибут унаследован от Windows API и связан с полем Subsystem в заголовке PE-файла. Rust транслирует его в флаги компоновщика через rustc. Без указания подсистемы используется значение по умолчанию — консольное приложение (SUBSYSTEM:CONSOLE).

Варианты для Windows

Существует несколько значений для windows_subsystem:

  1. console (по умолчанию): Запускает приложение с консольным окном. Подходит для CLI-приложений. Пример:
  2. #![windows_subsystem = "console"]
    fn main() {
        println!("С консолью!");
    }
  3. windows: Запускает приложение без консоли (GUI-режим). Используется для графических приложений. Пример:
  4. #![windows_subsystem = "windows"]
    fn main() {
        // Логика GUI
    }
  5. Менее распространённые варианты:

Как это работает?

Rust передаёт значение в linker через флаг -Wl,--subsystem,<value> (для MinGW) или эквивалент для MSVC. Например, #![windows_subsystem = "windows"] эквивалентно rustc -C link-args="-mwindows".

Аналоги для Linux и macOS

Для Unix-систем нет прямого аналога windows_subsystem, так как поведение приложения определяется:

Документация по атрибутам доступна здесь: Rustc attributes.

Сборка

Соберите проект для нужной цели с помощью команд:

cargo build --target x86_64-pc-windows-gnu --release
cargo build --target x86_64-unknown-linux-gnu --release

Бинарники будут находиться в папке target/<target>/release/.

Нюансы

Если вам нужен вывод в консоль в GUI-режиме на Windows, используйте функцию AllocConsole() из библиотеки winapi. Для Linux, чтобы уменьшить размер бинарника, используйте утилиту strip, например: strip target/x86_64-unknown-linux-gnu/release/myapp.


3. Особенности разных ОС (Windows, Linux, macOS): Пути, Symlink и Signal-Hook

Linux использует прямые слэши / в путях. Что это значит?

В Linux (и других POSIX-совместимых системах, таких как macOS) пути к файлам и директориям используют прямой слэш / как разделитель.

Это отличается от Windows, где традиционно используется обратный слэш \ (например, C:\Users\file.txt), хотя Windows также поддерживает / в некоторых случаях.

Пример пути в Linux: /home/user/docs/file.txt.

Почему это важно для Rust?

Rust предоставляет кроссплатформенные инструменты для работы с путями через модуль std::path. Использование этих инструментов позволяет избежать хардкода разделителей и делает код переносимым.

Например, вместо ручного конструирования пути с / или \, лучше использовать Path или PathBuf:

use std::path::Path;

let path = Path::new("/home/user/file.txt"); // Работает на Linux
println!("{:?}", path);

На Windows этот же код автоматически адаптируется к локальным разделителям, если используется Path::new.

Нюансы

Абсолютные и относительные пути:

Кодировка: Linux обычно использует UTF-8 для путей, но Rust обрабатывает их как OsString, что позволяет работать с не-UTF-8 именами файлов (хотя это редкость).

Кроссплатформенность: Если код должен работать на Windows и Linux, избегайте прямого использования / в строках. Вместо этого:

use std::path::PathBuf;

let mut path = PathBuf::new();
path.push("home");
path.push("user");
path.push("file.txt");
// На Linux: "home/user/file.txt"
// На Windows: "home\user\file.txt"

Поддержка символических ссылок доступна через std::os::unix::fs::symlink("src", "link").unwrap();. Что такое символические ссылки?

Символическая ссылка (symlink) — это специальный файл, который указывает на другой файл или директорию в файловой системе.

В Linux это аналог "ярлыков" в Windows, но более мощный и гибкий инструмент.

Пример в терминале:

ln -s /original/file.txt link.txt

Теперь link.txt указывает на /original/file.txt.

Как это работает в Rust?

Rust предоставляет функцию std::os::unix::fs::symlink для создания символических ссылок, но она доступна только на Unix-системах (Linux, macOS, BSD и т.д.), так как это POSIX-функциональность.

Синтаксис:

std::os::unix::fs::symlink("src", "link").unwrap();

Первый аргумент ("src") — путь к исходному файлу или директории (цель ссылки).

Второй аргумент ("link") — имя создаваемой ссылки.

Метод возвращает Result<(), std::io::Error>. unwrap() разворачивает результат, вызывая панику при ошибке.

Пример использования

use std::os::unix::fs;

fn main() {
    // Создаём символическую ссылку "link.txt", указывающую на "original.txt"
    match fs::symlink("original.txt", "link.txt") {
        Ok(()) => println!("Символическая ссылка создана!"),
        Err(e) => eprintln!("Ошибка: {}", e),
    }
}

Если original.txt не существует, ссылка всё равно создаётся (это "висячая ссылка", что допустимо в Linux).

После выполнения ls -l в терминале покажет: link.txt -> original.txt.

Нюансы

Платформозависимость:

std::os::unix::fs::symlink доступен только на Unix-системах. На Windows этот код не скомпилируется, если не использовать условную компиляцию:

#[cfg(target_family = "unix")]
fn create_symlink() {
    std::os::unix::fs::symlink("src", "link").unwrap();
}

Для Windows используйте std::os::windows::fs::symlink_file или symlink_dir, но с оговоркой: создание ссылок требует прав администратора (до Windows 10 1703) или включённого режима разработчика.

Ошибки:

Если файл link уже существует, symlink вернёт ошибку io::ErrorKind::AlreadyExists.

Используйте match или expect вместо unwrap() в production-коде для обработки ошибок.

Типы ссылок:

symlink создаёт мягкие (soft) ссылки. Жёсткие (hard) ссылки доступны через std::fs::hard_link.

Кроссплатформенный подход

Для поддержки и Windows, и Linux можно написать обёртку:

use std::path::Path;

fn create_symlink(src: &str, link: &str) -> std::io::Result<()> {
    #[cfg(target_family = "unix")]
    std::os::unix::fs::symlink(src, link)?;

    #[cfg(target_os = "windows")]
    {
        let src_path = Path::new(src);
        let link_path = Path::new(link);
        if src_path.is_dir() {
            std::os::windows::fs::symlink_dir(src, link)?;
        } else {
            std::os::windows::fs::symlink_file(src, link)?;
        }
    }

    Ok(())
}

fn main() {
    if let Err(e) = create_symlink("original.txt", "link.txt") {
        eprintln!("Ошибка: {}", e);
    }
}

Для обработки сигналов используйте crate signal-hook. Что такое сигналы в Linux?

Сигналы — это механизм в Unix-системах для передачи уведомлений процессам (например, о прерывании, завершении или ошибке).

Примеры сигналов:

В отличие от Windows, где используются события и сообщения, сигналы — ключевая часть POSIX-систем.

Почему нужен signal-hook?

Стандартная библиотека Rust (std) не предоставляет удобных средств для обработки сигналов, так как это платформозависимая функциональность.

Crate signal-hook — популярное решение для безопасной и удобной работы с сигналами в Rust.

Альтернативы: nix (низкоуровневый), tokio-signal (асинхронный).

Установка

Добавьте в Cargo.toml:

[dependencies]
signal-hook = "0.3"

Пример использования

use signal_hook::consts::SIGINT;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

fn main() -> std::io::Result<()> {
    // Флаг для отслеживания сигнала
    let running = Arc::new(AtomicBool::new(true));
    let r = running.clone();

    // Регистрация обработчика SIGINT
    signal_hook::flag::register(SIGINT, running)?;

    println!("Нажмите Ctrl+C для завершения...");
    while r.load(Ordering::Relaxed) {
        std::thread::sleep(std::time::Duration::from_secs(1));
        println!("Работаю...");
    }
    println!("Получен SIGINT, завершаю работу.");
    Ok(())
}

Что происходит?

register связывает сигнал SIGINT с флагом running.

При получении SIGINT (Ctrl+C) флаг становится false, прерывая цикл.

Вывод:

Нажмите Ctrl+C для завершения...
Работаю...
Работаю...
^CПолучен SIGINT, завершаю работу.

Нюансы

Безопасность: signal-hook минимизирует риски, связанные с сигналами (например, race conditions), но обработчик должен быть минимальным — избегайте сложной логики.

Множественные сигналы:

use signal_hook::consts::{SIGINT, SIGTERM};
use signal_hook::iterator::Signals;

fn main() -> std::io::Result<()> {
    let mut signals = Signals::new(&[SIGINT, SIGTERM])?;
    for sig in signals.forever() {
        println!("Получен сигнал: {:?}", sig);
        break;
    }
    Ok(())
}

Signals::new создаёт итератор для обработки нескольких сигналов.

Кроссплатформенность: Сигналы — это Unix-функциональность. На Windows используйте ctrlc crate:

#[cfg(target_os = "windows")]
use ctrlc;

#[cfg(target_os = "windows")]
fn handle_signals() {
    ctrlc::set_handler(|| {
        println!("Получен Ctrl+C на Windows!");
        std::process::exit(0);
    }).expect("Ошибка установки обработчика");
}

Итог

Пути: Linux использует /, и Rust помогает абстрагироваться от платформ через std::path.

Символические ссылки: std::os::unix::fs::symlink — простой способ их создания в Unix, но требует условной компиляции для кроссплатформенности.

Сигналы: signal-hook — удобный и безопасный crate для их обработки в Linux, в отличие от Windows, где нужны другие подходы.


4. Тестирование на разных платформах

Локальное тестирование

Запускайте тесты для каждой цели с помощью команд:

cargo test --target x86_64-pc-windows-gnu
cargo test --target x86_64-unknown-linux-gnu

Давайте подробно разберём, что происходит при выполнении команд

Эти команды запускают тестирование проекта Rust с использованием кросс-компиляции для указанных целей (targets). Чтобы понять процесс, разобьём его на шаги и рассмотрим, что делает cargo test, как работает флаг --target, и какие нюансы возникают при выполнении тестов для разных платформ.

Что делает cargo test?

cargo test — это команда в Cargo (системе сборки Rust), которая выполняет следующие действия:

  1. Компиляция тестов: Собирает все тесты, определённые в проекте. Тесты обычно находятся в модулях с атрибутом #[cfg(test)] или в файлах в папке tests/.
  2. Компиляция основного кода: Если тесты зависят от кода в src/ (например, через use crate::...), этот код тоже компилируется.
  3. Запуск тестов: После успешной компиляции Cargo запускает скомпилированные тестовые бинарники и выводит результаты (успех, провал, пропущенные тесты).

По умолчанию cargo test использует хост-платформу (ту, на которой вы работаете, например, x86_64-unknown-linux-gnu на Linux или x86_64-pc-windows-msvc на Windows).

Роль флага --target

Флаг --target указывает Cargo, для какой платформы нужно компилировать код. В данном случае:

Когда вы добавляете --target, Cargo переключается с хост-платформы на указанную цель, что означает кросс-компиляцию. Это полезно для тестирования кода на платформах, отличных от той, на которой вы работаете.

Что происходит шаг за шагом?

> Проверка зависимостей и конфигурации

Cargo проверяет, установлен ли нужный target через rustup. Если нет, вы получите ошибку вроде:

error: target 'x86_64-pc-windows-gnu' is not installed

Исправление: rustup target add x86_64-pc-windows-gnu.

Проверяется наличие подходящего linker’а (компоновщика) для цели. Например:

Если linker не настроен, это можно указать в .cargo/config.toml:

[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"

> Компиляция тестов

Cargo компилирует исходный код и тесты для указанной цели.

Используется rustc с флагом --target, например:

rustc --target x86_64-pc-windows-gnu ...

Выходные файлы (тестовые бинарники) сохраняются в:

Если в коде есть условная компиляция (например, #[cfg(target_os = "windows")]), то только соответствующие блоки будут включены в сборку для этой цели.

> Запуск тестов (или попытка запуска)

После компиляции Cargo пытается запустить скомпилированные тестовые бинарники.

Важный нюанс: Cargo ожидает, что тесты можно выполнить на текущей хост-системе. Однако:

Результат:

> Вывод результатов

Если тесты не могут быть запущены из-за кросс-платформенности, Cargo сообщит об этом.

Если тесты запускаются (на хосте или через эмуляцию), вы увидите стандартный вывод:

running 2 tests
test test_one ... ok
test test_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored

Нюансы и ограничения

  1. Кросс-компиляция ≠ кросс-запуск:
  2. Зависимости:
  3. Поведение по умолчанию:

Как запустить тесты для кросс-платформ?

Чтобы действительно протестировать код на разных платформах, нужно:

  1. Эмуляция:
  2. Физические машины/VM:

Пример

Допустим, у вас есть код:

#[cfg(test)]
mod tests {
    #[test]
    fn test_windows() {
        #[cfg(target_os = "windows")]
        assert_eq!(std::env::var("OS").unwrap(), "Windows_NT");
    }

    #[test]
    fn test_linux() {
        #[cfg(target_os = "linux")]
        assert!(std::env::var("HOME").is_ok());
    }
}

Итого


5. CI/CD с GitHub Actions

CI/CD — это автоматизация сборки, тестирования и деплоя. GitHub Actions — популярный инструмент для этого.

Базовый workflow

Вот пример базового workflow для проверки кода на разных ОС:

name: Rust CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    steps:
      - uses: actions/checkout@v3
      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
      - name: Build
        run: cargo build --verbose
      - name: Run tests
        run: cargo test --verbose

Расширенный пример с кросс-компиляцией

Этот пример демонстрирует кросс-компиляцию и сохранение бинарников:

name: Cross-Platform CI
on: [push, pull_request]

jobs:
  cross-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: x86_64-pc-windows-gnu, x86_64-unknown-linux-gnu, x86_64-apple-darwin
      - name: Install MinGW
        run: sudo apt update && sudo apt install -y mingw-w64
      - name: Build for Windows
        run: cargo build --target x86_64-pc-windows-gnu --release
      - name: Build for Linux
        run: cargo build --target x86_64-unknown-linux-gnu --release
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: binaries
          path: |
            target/x86_64-pc-windows-gnu/release/*.exe
            target/x86_64-unknown-linux-gnu/release/*

Кэширование

Кэширование в GitHub Actions — это механизм, который позволяет сохранять файлы или папки между выполнениями workflow (jobs) и повторно использовать их в последующих запусках. Это особенно полезно для проектов с большими зависимостями или длительными процессами компиляции, таких как Rust-проекты, где загрузка crates и сборка могут занимать значительное время.

Кэширование ускоряет сборку. Добавьте следующий шаг в workflow:

- name: Cache cargo
  uses: actions/cache@v3
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      target
    key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

Этот шаг добавляется в workflow GitHub Actions, чтобы сохранить и повторно использовать данные между запусками сборки, минимизируя время на повторную загрузку зависимостей и компиляцию.

1. - name: Cache cargo

Это просто название шага, которое отображается в логах GitHub Actions.

Оно не влияет на функциональность, но помогает понять, что делает этот шаг (кэширование для Cargo).

2. uses: actions/cache@v3

Указывает, что используется действие (action) actions/cache версии 3.

actions/cache — официальное действие от GitHub для работы с кэшированием.

@v3 — конкретная версия действия (на март 2025 года это актуальная версия; в будущем может быть @v4 или новее).

Действие отвечает за сохранение и восстановление файлов, указанных в path, на основе ключа key.

3. with:

Блок with передаёт параметры для действия actions/cache@v3.

a) path: |

Определяет, какие файлы или папки будут кэшироваться. Используется многострочный синтаксис YAML (|) для удобства.

Указанные пути:

b) key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

key — это уникальный идентификатор кэша. Если ключ совпадает с предыдущим запуском, кэш восстанавливается; если нет — создаётся новый.

Разберём его состав:

Пример ключа:

Как это ускоряет сборку?

  1. Без кэширования:
  2. С кэшированием:

Экономия: До 90% времени сборки в больших проектах с неизменёнными зависимостями.

Пример интеграции в workflow

Полный пример workflow с кэшированием:

name: Rust CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Cache cargo
        uses: actions/cache@v3
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
      - name: Build
        run: cargo build --verbose
      - name: Test
        run: cargo test --verbose

Порядок важен: Кэширование должно быть после checkout (чтобы получить Cargo.lock), но перед cargo build.

Нюансы и тонкости

  1. Частичное совпадение кэша:
  2. Размер кэша:
  3. Устаревание кэша:
  4. Платформозависимость:
  5. Дополнительные параметры (опционально):

Итог кэштрования

Добавление шага:

- name: Cache cargo
  uses: actions/cache@v3
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      target
    key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

ускоряет сборку Rust-проектов в GitHub Actions, сохраняя зависимости и скомпилированные артефакты между запусками. Оно особенно эффективно для проектов с большим количеством зависимостей или частыми CI/CD-запусками, где повторная компиляция с нуля была бы затратной. Настройка ключа с учётом Cargo.lock гарантирует, что кэш обновляется только при необходимости.

Тестирование бинарников


6. Примеры: сборка для Windows и Linux

Создадим проект с платформозависимым кодом:

#![cfg(target_os = "windows")]
#![windows_subsystem = "windows"]

use std::fs;

fn main() {
    #[cfg(target_os = "windows")]
    fs::write("out.txt", "Windows!\r\n").unwrap();

    #[cfg(target_os = "linux")]
    fs::write("out.txt", "Linux!\n").unwrap();
}

Сборка проекта:

cargo build --target x86_64-pc-windows-gnu --release
cargo build --target x86_64-unknown-linux-gnu --release

7. Упражнение: Настроить проект для двух платформ

Задание

Создайте проект с GUI-режимом на Windows и консолью на Linux.

Решение

Код для файла src/main.rs:

#![cfg(target_os = "windows")]
#![windows_subsystem = "windows"]

fn main() {
    #[cfg(target_os = "windows")]
    let msg = "Windows GUI mode";

    #[cfg(target_os = "linux")]
    let msg = format!("Linux kernel: {}", std::fs::read_to_string("/proc/version").unwrap_or_default());

    println!("{}", msg);
}

Настройка в файле .cargo/config.toml:

[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
rustflags = ["-C", "link-args=-mwindows"]

[target.x86_64-unknown-linux-gnu]
linker = "gcc"

Сборка и запуск:

cargo run --target x86_64-pc-windows-gnu
cargo run --target x86_64-unknown-linux-gnu

Заключение

Мы подробно разобрали кроссплатформенную разработку в Rust, включая windows_subsystem и CI/CD с GitHub Actions. Теперь вы можете создавать и тестировать приложения для любых платформ!