Add .NET tool packaging and NuGet release workflow for smb CLI
- Add nuget/smbcloud-cli .NET tool launcher and packaging - Add release-nuget.yml workflow for cross-platform native builds and NuGet publish - Update badges in README files to include NuGet - Document .NET tool usage and installation
keypair34 committed
May 2, 2026 at 19:02 UTC
d5c477ad4cf9c44e6cc98cf28fb4972b499e8d7d
9 files changed
+518
.github/workflows/ci.yml
+17
@@ -7,6 +7,7 @@ name: Tests
7
# - sdk/python → maturin wheel build (smbcloud-sdk-auth)
8
# - sdk/npm → wasm-pack + prepare-package.mjs (@smbcloud/sdk-auth)
9
# - sdk/gems → rb_sys native-extension compile (smbcloud-auth gem)
10
+# - nuget → .NET tool launcher build (SmbCloud.Cli)
11
12
on: push
13
@@ -170,6 +171,22 @@ jobs:
171
working-directory: sdk/npm/smbcloud-auth
172
run: node ./prepare-package.mjs
173
174
+ # ── .NET tool ───────────────────────────────────────────────────────────────
175
+ check-nuget-tool:
176
+ name: NuGet — .NET tool launcher (SmbCloud.Cli)
177
+ runs-on: ubuntu-latest
178
+ steps:
179
+ - name: Checkout
180
+ uses: actions/checkout@v6
181
+
182
+ - name: Setup .NET SDK
183
+ uses: actions/setup-dotnet@v4
184
+ with:
185
+ dotnet-version: 8.0.x
186
+
187
+ - name: Build .NET tool launcher
188
+ run: dotnet build nuget/smbcloud-cli/SmbCloud.Cli.csproj --configuration Release
189
+
190
# ── Ruby Gem ────────────────────────────────────────────────────────────────
191
check-ruby-gem:
192
name: SDK — Ruby gem (smbcloud-auth)
.github/workflows/release-nuget.yml
new
+293
@@ -0,0 +1,293 @@
1
+name: NuGet CLI Release
2
+
3
+on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+ workflow_dispatch:
8
+ inputs:
9
+ tag:
10
+ description: "Release tag (e.g. v0.3.38)"
11
+ required: true
12
+
13
+permissions:
14
+ contents: read
15
+
16
+env:
17
+ CARGO_TERM_COLOR: always
18
+ CLI_CLIENT_SECRET: ${{ secrets.CLI_CLIENT_SECRET }}
19
+ DOTNET_VERSION: 8.0.x
20
+ NUGET_PACKAGE_ID: SmbCloud.Cli
21
+
22
+jobs:
23
+ build-native-binaries:
24
+ name: Build native binary (${{ matrix.build.NAME }})
25
+ runs-on: ${{ matrix.build.OS }}
26
+ strategy:
27
+ fail-fast: false
28
+ matrix:
29
+ build:
30
+ - {
31
+ NAME: linux-x64,
32
+ OS: ubuntu-latest,
33
+ TARGET: x86_64-unknown-linux-gnu,
34
+ }
35
+ - {
36
+ NAME: linux-arm64,
37
+ OS: ubuntu-24.04-arm,
38
+ TARGET: aarch64-unknown-linux-gnu,
39
+ }
40
+ - {
41
+ NAME: windows-x64,
42
+ OS: windows-2022,
43
+ TARGET: x86_64-pc-windows-msvc,
44
+ }
45
+ - {
46
+ NAME: windows-arm64,
47
+ OS: windows-2022,
48
+ TARGET: aarch64-pc-windows-msvc,
49
+ }
50
+ - {
51
+ NAME: darwin-x64,
52
+ OS: macos-15-intel,
53
+ TARGET: x86_64-apple-darwin,
54
+ }
55
+ - {
56
+ NAME: darwin-arm64,
57
+ OS: macos-latest,
58
+ TARGET: aarch64-apple-darwin,
59
+ }
60
+ steps:
61
+ - name: Checkout
62
+ uses: actions/checkout@v6
63
+ with:
64
+ ref: ${{ github.event.inputs.tag || github.ref }}
65
+
66
+ - name: Set the release version
67
+ shell: bash
68
+ run: |
69
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
70
+ release_version="${GITHUB_REF_NAME#v}"
71
+ else
72
+ release_version="${{ github.event.inputs.tag }}"
73
+ release_version="${release_version#v}"
74
+ fi
75
+
76
+ echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
77
+
78
+ - name: Read Rust toolchain
79
+ shell: bash
80
+ run: |
81
+ rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
82
+ if [ -z "$rust_toolchain" ]; then
83
+ echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
84
+ exit 1
85
+ fi
86
+
87
+ echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
88
+
89
+ - name: Install toolkit
90
+ uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
91
+ with:
92
+ toolchain: ${{ env.RUST_TOOLCHAIN }}
93
+ targets: ${{ matrix.build.TARGET }}
94
+
95
+ - name: Setup Rust cache
96
+ uses: Swatinem/rust-cache@v2
97
+ with:
98
+ key: nuget-${{ matrix.build.TARGET }}
99
+
100
+ - name: Build
101
+ env:
102
+ GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
103
+ GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
104
+ GH_OAUTH_REDIRECT_URI: ${{ secrets.GH_OAUTH_REDIRECT_URI }}
105
+ shell: bash
106
+ run: cargo build --release --locked --target ${{ matrix.build.TARGET }} --package smbcloud-cli
107
+
108
+ - name: Prepare artifact
109
+ shell: bash
110
+ run: |
111
+ executable_name="smb"
112
+ if [[ "${{ matrix.build.OS }}" == "windows-2022" ]]; then
113
+ executable_name="smb.exe"
114
+ fi
115
+
116
+ mkdir -p "nuget-artifacts/native/${{ matrix.build.NAME }}"
117
+ cp "target/${{ matrix.build.TARGET }}/release/${executable_name}" "nuget-artifacts/native/${{ matrix.build.NAME }}/${executable_name}"
118
+
119
+ - name: Upload native artifact
120
+ uses: actions/upload-artifact@v7
121
+ with:
122
+ name: nuget-native-${{ matrix.build.NAME }}
123
+ path: nuget-artifacts/native
124
+
125
+ pack-nuget:
126
+ name: Pack NuGet .NET tool
127
+ runs-on: ubuntu-latest
128
+ needs: build-native-binaries
129
+ steps:
130
+ - name: Checkout
131
+ uses: actions/checkout@v6
132
+ with:
133
+ ref: ${{ github.event.inputs.tag || github.ref }}
134
+
135
+ - name: Set the release version
136
+ shell: bash
137
+ run: |
138
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
139
+ release_version="${GITHUB_REF_NAME#v}"
140
+ else
141
+ release_version="${{ github.event.inputs.tag }}"
142
+ release_version="${release_version#v}"
143
+ fi
144
+
145
+ echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
146
+
147
+ - name: Download native artifacts
148
+ uses: actions/download-artifact@v7
149
+ with:
150
+ pattern: nuget-native-*
151
+ path: nuget/smbcloud-cli
152
+ merge-multiple: true
153
+
154
+ - name: Setup .NET SDK
155
+ uses: actions/setup-dotnet@v4
156
+ with:
157
+ dotnet-version: ${{ env.DOTNET_VERSION }}
158
+
159
+ - name: Pack .NET tool
160
+ shell: bash
161
+ run: |
162
+ dotnet pack nuget/smbcloud-cli/SmbCloud.Cli.csproj \
163
+ --configuration Release \
164
+ --output nuget/dist \
165
+ -p:PackageVersion=${RELEASE_VERSION}
166
+
167
+ - name: Upload package artifact
168
+ uses: actions/upload-artifact@v7
169
+ with:
170
+ name: nuget-package
171
+ path: nuget/dist/*.nupkg
172
+
173
+ smoke-test:
174
+ name: Smoke test (${{ matrix.os }})
175
+ runs-on: ${{ matrix.os }}
176
+ needs: pack-nuget
177
+ strategy:
178
+ fail-fast: false
179
+ matrix:
180
+ os:
181
+ - ubuntu-latest
182
+ - windows-2022
183
+ - macos-latest
184
+ steps:
185
+ - name: Checkout
186
+ uses: actions/checkout@v6
187
+ with:
188
+ ref: ${{ github.event.inputs.tag || github.ref }}
189
+
190
+ - name: Set the release version
191
+ shell: bash
192
+ run: |
193
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
194
+ release_version="${GITHUB_REF_NAME#v}"
195
+ else
196
+ release_version="${{ github.event.inputs.tag }}"
197
+ release_version="${release_version#v}"
198
+ fi
199
+
200
+ echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
201
+
202
+ - name: Download package artifact
203
+ uses: actions/download-artifact@v7
204
+ with:
205
+ name: nuget-package
206
+ path: nuget/dist
207
+
208
+ - name: Setup .NET SDK
209
+ uses: actions/setup-dotnet@v4
210
+ with:
211
+ dotnet-version: ${{ env.DOTNET_VERSION }}
212
+
213
+ - name: Install tool from local package
214
+ shell: pwsh
215
+ run: |
216
+ $packageSource = (Resolve-Path "nuget/dist").Path
217
+ @"
218
+ <?xml version="1.0" encoding="utf-8"?>
219
+ <configuration>
220
+ <packageSources>
221
+ <clear />
222
+ <add key="local" value="$packageSource" />
223
+ </packageSources>
224
+ </configuration>
225
+ "@ | Set-Content -Path "nuget/NuGet.Config"
226
+
227
+ dotnet tool install `
228
+ --tool-path ".tool" `
229
+ --configfile "nuget/NuGet.Config" `
230
+ --version "$env:RELEASE_VERSION" `
231
+ "$env:NUGET_PACKAGE_ID"
232
+
233
+ - name: Run smb --version
234
+ shell: pwsh
235
+ run: |
236
+ $toolPath = if ($IsWindows) { ".\.tool\smb.exe" } else { "./.tool/smb" }
237
+ & $toolPath --version
238
+
239
+ publish-nuget:
240
+ name: Publish to NuGet
241
+ runs-on: ubuntu-latest
242
+ needs: smoke-test
243
+ steps:
244
+ - name: Checkout
245
+ uses: actions/checkout@v6
246
+ with:
247
+ ref: ${{ github.event.inputs.tag || github.ref }}
248
+
249
+ - name: Set the release version
250
+ shell: bash
251
+ run: |
252
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
253
+ release_version="${GITHUB_REF_NAME#v}"
254
+ else
255
+ release_version="${{ github.event.inputs.tag }}"
256
+ release_version="${release_version#v}"
257
+ fi
258
+
259
+ echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
260
+
261
+ - name: Download package artifact
262
+ uses: actions/download-artifact@v7
263
+ with:
264
+ name: nuget-package
265
+ path: nuget/dist
266
+
267
+ - name: Setup .NET SDK
268
+ uses: actions/setup-dotnet@v4
269
+ with:
270
+ dotnet-version: ${{ env.DOTNET_VERSION }}
271
+
272
+ - name: Check whether release already exists on NuGet
273
+ id: nuget-check
274
+ shell: bash
275
+ run: |
276
+ package_index_url="https://api.nuget.org/v3-flatcontainer/smbcloud.cli/index.json"
277
+ if curl -fsS "${package_index_url}" | grep -F "\"${RELEASE_VERSION}\"" >/dev/null; then
278
+ echo "${NUGET_PACKAGE_ID} ${RELEASE_VERSION} already exists on NuGet, skipping publish"
279
+ echo "exists=true" >> "$GITHUB_OUTPUT"
280
+ else
281
+ echo "exists=false" >> "$GITHUB_OUTPUT"
282
+ fi
283
+
284
+ - name: Publish package
285
+ if: steps.nuget-check.outputs.exists != 'true'
286
+ env:
287
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
288
+ shell: bash
289
+ run: |
290
+ dotnet nuget push nuget/dist/*.nupkg \
291
+ --api-key "${NUGET_API_KEY}" \
292
+ --source https://api.nuget.org/v3/index.json \
293
+ --skip-duplicate
README.MD
+7
@@ -19,6 +19,7 @@
19
<p align="center">
20
<a href="https://crates.io/crates/smbcloud-cli"><img alt="Crates.io" src="https://img.shields.io/crates/v/smbcloud-cli"></a>
21
<a href="https://www.npmjs.com/package/@smbcloud/cli"><img alt="npm" src="https://img.shields.io/npm/v/@smbcloud/cli"></a>
22
+ <a href="https://www.nuget.org/packages/SmbCloud.Cli"><img alt="NuGet" src="https://img.shields.io/nuget/v/SmbCloud.Cli"></a>
23
<a href="https://github.com/smbcloudXYZ/homebrew-tap"><img alt="Homebrew" src="https://img.shields.io/badge/homebrew-tap-orange?logo=homebrew"></a>
24
<a href="https://pypi.org/project/smbcloud-cli/"><img alt="PyPI" src="https://img.shields.io/pypi/v/smbcloud-cli"></a>
25
<a href="https://github.com/smbcloudXYZ/smbcloud-cli/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/smbcloudXYZ/smbcloud-cli"></a>
@@ -48,6 +49,12 @@ brew install cli
49
npm install -g @smbcloud/cli
50
```
51
52
+### .NET tool
53
+
54
+```sh
55
+dotnet tool install --global SmbCloud.Cli
56
+```
57
+
58
### pip
59
60
```sh
npm/smbcloud-cli/README.md
+7
@@ -18,6 +18,7 @@
18
</p>
19
<p align="center">
20
<a href="https://www.npmjs.com/package/@smbcloud/cli"><img alt="npm" src="https://img.shields.io/npm/v/@smbcloud/cli"></a>
21
+ <a href="https://www.nuget.org/packages/SmbCloud.Cli"><img alt="NuGet" src="https://img.shields.io/nuget/v/SmbCloud.Cli"></a>
22
<a href="https://pypi.org/project/smbcloud-cli/"><img alt="PyPI" src="https://img.shields.io/pypi/v/smbcloud-cli"></a>
23
<a href="https://github.com/smbcloudXYZ/smbcloud-cli/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/smbcloudXYZ/smbcloud-cli"></a>
24
</p>
@@ -56,6 +57,12 @@ brew tap smbcloudXYZ/tap
57
brew install cli
58
```
59
60
+### .NET tool
61
+
62
+```sh
63
+dotnet tool install --global SmbCloud.Cli
64
+```
65
+
66
### pip
67
68
```sh
nuget/.gitignore
new
+4
@@ -0,0 +1,4 @@
1
+dist/
2
+smbcloud-cli/bin/
3
+smbcloud-cli/obj/
4
+smbcloud-cli/native/
nuget/smbcloud-cli/Program.cs
new
+109
@@ -0,0 +1,109 @@
1
+using System.ComponentModel;
2
+using System.Diagnostics;
3
+using System.Runtime.InteropServices;
4
+
5
+internal static class Program
6
+{
7
+ private const string CommandName = "smb";
8
+
9
+ public static async Task<int> Main(string[] arguments)
10
+ {
11
+ try
12
+ {
13
+ string executablePath = ResolveExecutablePath();
14
+ EnsureExecutablePermissions(executablePath);
15
+ return await RunAsync(executablePath, arguments);
16
+ }
17
+ catch (Exception exception) when (
18
+ exception is FileNotFoundException or
19
+ PlatformNotSupportedException or
20
+ Win32Exception)
21
+ {
22
+ Console.Error.WriteLine($"{CommandName}: {exception.Message}");
23
+ return 1;
24
+ }
25
+ }
26
+
27
+ private static string ResolveExecutablePath()
28
+ {
29
+ string runtimeIdentifier = GetRuntimeIdentifier();
30
+ string executableName = OperatingSystem.IsWindows() ? $"{CommandName}.exe" : CommandName;
31
+ string executablePath = Path.GetFullPath(
32
+ Path.Combine(AppContext.BaseDirectory, "native", runtimeIdentifier, executableName));
33
+
34
+ if (!File.Exists(executablePath))
35
+ {
36
+ throw new FileNotFoundException(
37
+ $"The native {CommandName} executable for '{runtimeIdentifier}' is not bundled in this package.",
38
+ executablePath);
39
+ }
40
+
41
+ return executablePath;
42
+ }
43
+
44
+ private static string GetRuntimeIdentifier()
45
+ {
46
+ string operatingSystem = OperatingSystem.IsWindows()
47
+ ? "windows"
48
+ : OperatingSystem.IsMacOS()
49
+ ? "darwin"
50
+ : OperatingSystem.IsLinux()
51
+ ? "linux"
52
+ : throw new PlatformNotSupportedException(
53
+ $"{CommandName} does not support this operating system through the .NET tool package.");
54
+
55
+ string architecture = RuntimeInformation.ProcessArchitecture switch
56
+ {
57
+ Architecture.X64 => "x64",
58
+ Architecture.Arm64 => "arm64",
59
+ _ => throw new PlatformNotSupportedException(
60
+ $"{CommandName} does not support the '{RuntimeInformation.ProcessArchitecture}' architecture through the .NET tool package."),
61
+ };
62
+
63
+ return $"{operatingSystem}-{architecture}";
64
+ }
65
+
66
+ private static void EnsureExecutablePermissions(string executablePath)
67
+ {
68
+ if (OperatingSystem.IsWindows())
69
+ {
70
+ return;
71
+ }
72
+
73
+ UnixFileMode currentMode = File.GetUnixFileMode(executablePath);
74
+ UnixFileMode requiredMode = UnixFileMode.UserRead |
75
+ UnixFileMode.UserWrite |
76
+ UnixFileMode.UserExecute |
77
+ UnixFileMode.GroupRead |
78
+ UnixFileMode.GroupExecute |
79
+ UnixFileMode.OtherRead |
80
+ UnixFileMode.OtherExecute;
81
+
82
+ if ((currentMode & requiredMode) == requiredMode)
83
+ {
84
+ return;
85
+ }
86
+
87
+ File.SetUnixFileMode(executablePath, currentMode | requiredMode);
88
+ }
89
+
90
+ private static async Task<int> RunAsync(string executablePath, IReadOnlyList<string> arguments)
91
+ {
92
+ ProcessStartInfo startInfo = new(executablePath)
93
+ {
94
+ UseShellExecute = false,
95
+ WorkingDirectory = Environment.CurrentDirectory,
96
+ };
97
+
98
+ foreach (string argument in arguments)
99
+ {
100
+ startInfo.ArgumentList.Add(argument);
101
+ }
102
+
103
+ using Process process = Process.Start(startInfo)
104
+ ?? throw new Win32Exception($"Failed to start '{CommandName}'.");
105
+
106
+ await process.WaitForExitAsync();
107
+ return process.ExitCode;
108
+ }
109
+}
nuget/smbcloud-cli/README.md
new
+48
@@ -0,0 +1,48 @@
1
+# smbCloud CLI for .NET
2
+
3
+`smb` is the official command-line interface for [smbCloud](https://smbcloud.xyz/).
4
+
5
+## Install
6
+
7
+```sh
8
+dotnet tool install --global SmbCloud.Cli
9
+```
10
+
11
+## Update
12
+
13
+```sh
14
+dotnet tool update --global SmbCloud.Cli
15
+```
16
+
17
+## Quick start
18
+
19
+```sh
20
+smb login
21
+smb init
22
+smb deploy
23
+```
24
+
25
+## Platform support
26
+
27
+This .NET tool bundles native `smb` binaries for:
28
+
29
+- macOS `arm64`, `x64`
30
+- Linux `arm64`, `x64` (glibc)
31
+- Windows `arm64`, `x64`
32
+
33
+## Other installation methods
34
+
35
+- npm: `npm install -g @smbcloud/cli`
36
+- pip: `pip install smbcloud-cli`
37
+- Homebrew: `brew tap smbcloudXYZ/tap && brew install cli`
38
+- GitHub Releases: <https://github.com/smbcloudXYZ/smbcloud-cli/releases>
39
+
40
+## Source
41
+
42
+- Repository: <https://github.com/smbcloudXYZ/smbcloud-cli>
43
+- Documentation: <https://smbcloud.xyz/posts>
44
+- Issues: <https://github.com/smbcloudXYZ/smbcloud-cli/issues>
45
+
46
+## License
47
+
48
+Apache-2.0
nuget/smbcloud-cli/SmbCloud.Cli.csproj
new
+26
@@ -0,0 +1,26 @@
1
+<Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <OutputType>Exe</OutputType>
4
+ <TargetFramework>net8.0</TargetFramework>
5
+ <ImplicitUsings>enable</ImplicitUsings>
6
+ <Nullable>enable</Nullable>
7
+ <PackAsTool>true</PackAsTool>
8
+ <ToolCommandName>smb</ToolCommandName>
9
+ <PackageId>SmbCloud.Cli</PackageId>
10
+ <PackageVersion>0.0.0-local</PackageVersion>
11
+ <Version>$(PackageVersion)</Version>
12
+ <Authors>Seto Elkahfi</Authors>
13
+ <Description>smbCloud command line interface.</Description>
14
+ <PackageReadmeFile>README.md</PackageReadmeFile>
15
+ <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
16
+ <PackageProjectUrl>https://smbcloud.xyz/</PackageProjectUrl>
17
+ <RepositoryUrl>https://github.com/smbcloudXYZ/smbcloud-cli</RepositoryUrl>
18
+ <PackageTags>cli;smbcloud;cloud;deployment;developer-tools;dotnet-tool</PackageTags>
19
+ <PublishRepositoryUrl>true</PublishRepositoryUrl>
20
+ </PropertyGroup>
21
+
22
+ <ItemGroup>
23
+ <None Include="README.md" Pack="true" PackagePath="" />
24
+ <None Include="native/**/*" Pack="true" PackagePath="tools/$(TargetFramework)/any/native/%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" Visible="false" />
25
+ </ItemGroup>
26
+</Project>
pypi/README.md
+7
@@ -19,6 +19,7 @@
19
<p align="center">
20
<a href="https://crates.io/crates/smbcloud-cli"><img alt="Crates.io" src="https://img.shields.io/crates/v/smbcloud-cli"></a>
21
<a href="https://www.npmjs.com/package/@smbcloud/cli"><img alt="npm" src="https://img.shields.io/npm/v/@smbcloud/cli"></a>
22
+ <a href="https://www.nuget.org/packages/SmbCloud.Cli"><img alt="NuGet" src="https://img.shields.io/nuget/v/SmbCloud.Cli"></a>
23
<a href="https://github.com/smbcloudXYZ/homebrew-tap"><img alt="Homebrew" src="https://img.shields.io/badge/homebrew-tap-orange?logo=homebrew"></a>
24
<a href="https://pypi.org/project/smbcloud-cli/"><img alt="PyPI" src="https://img.shields.io/pypi/v/smbcloud-cli"></a>
25
<a href="https://github.com/smbcloudXYZ/smbcloud-cli/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/smbcloudXYZ/smbcloud-cli"></a>
@@ -64,6 +65,12 @@ brew install cli
65
npm install -g @smbcloud/cli
66
```
67
68
+### .NET tool
69
+
70
+```sh
71
+dotnet tool install --global SmbCloud.Cli
72
+```
73
+
74
### Shell (macOS / Linux)
75
76
```sh