├── .clang-format ├── .github ├── scripts │ └── setenv.ps1 └── workflows │ ├── compile.yml │ └── release.yml ├── .gitignore ├── LICENSE-MIT ├── LICENSE-UNLICENSE ├── Makefile ├── README.md ├── build ├── build.ninja ├── build.ps1 ├── build.zig └── shim.vcxproj ├── compile_flags.txt ├── repshims.bat ├── shim.cpp ├── shim.manifest └── version /.clang-format: -------------------------------------------------------------------------------- 1 | AccessModifierOffset: -4 2 | AlignAfterOpenBracket: AlwaysBreak 3 | AlignConsecutiveAssignments: false 4 | AlignConsecutiveDeclarations: false 5 | AlignEscapedNewlines: DontAlign 6 | AlignTrailingComments: false 7 | AllowAllParametersOfDeclarationOnNextLine: false 8 | AllowShortFunctionsOnASingleLine: true 9 | AllowShortIfStatementsOnASingleLine: false 10 | AllowShortLoopsOnASingleLine: false 11 | AlwaysBreakAfterReturnType: None 12 | AlwaysBreakBeforeMultilineStrings: false 13 | AlwaysBreakTemplateDeclarations: true 14 | BinPackArguments: false 15 | BinPackParameters: false 16 | BreakBeforeBinaryOperators: None 17 | 18 | # Custom format to allow namespace and empty method compression. 19 | BreakBeforeBraces: Custom 20 | BraceWrapping: 21 | AfterClass: true 22 | AfterControlStatement: true 23 | AfterEnum : true 24 | AfterExternBlock: true 25 | AfterFunction: true 26 | AfterNamespace: false 27 | AfterStruct: true 28 | AfterUnion: true 29 | BeforeCatch: true 30 | BeforeElse: true 31 | IndentBraces: false 32 | SplitEmptyFunction: false 33 | SplitEmptyRecord: false 34 | SplitEmptyNamespace: false 35 | 36 | BreakBeforeTernaryOperators: false 37 | BreakConstructorInitializers: AfterColon 38 | BreakStringLiterals: true 39 | ColumnLimit: 160 40 | CompactNamespaces: true 41 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 42 | ConstructorInitializerIndentWidth: 4 43 | ContinuationIndentWidth: 4 44 | Cpp11BracedListStyle: true 45 | DerivePointerBinding: false 46 | FixNamespaceComments: true 47 | IncludeBlocks: Preserve 48 | IndentCaseLabels: false 49 | IndentPPDirectives: None 50 | IndentWidth: 4 51 | IndentWrappedFunctionNames: false 52 | Language: Cpp 53 | MaxEmptyLinesToKeep: 1 54 | NamespaceIndentation: None 55 | PointerAlignment: Left 56 | ReflowComments: false 57 | SortIncludes: false 58 | SortUsingDeclarations: true 59 | SpaceAfterCStyleCast: false 60 | SpaceAfterTemplateKeyword: false 61 | SpaceBeforeAssignmentOperators: true 62 | SpaceBeforeCpp11BracedList: true 63 | SpaceBeforeParens: ControlStatements 64 | SpaceInEmptyParentheses: false 65 | SpacesBeforeTrailingComments: 1 66 | SpacesInAngles: false 67 | SpacesInCStyleCastParentheses: false 68 | SpacesInContainerLiterals: false 69 | SpacesInParentheses: false 70 | SpacesInSquareBrackets: false 71 | Standard: Cpp11 72 | TabWidth: 4 73 | UseTab: Never 74 | MacroBlockBegin: '^BEGIN_MODULE$|^BEGIN_TEST_CLASS$|^BEGIN_TEST_METHOD$|^BEGIN_COM_MAP$' 75 | MacroBlockEnd: '^END_MODULE$|^END_TEST_CLASS$|^END_TEST_METHOD$|^END_TEST_METHOD$|^END_COM_MAP$' -------------------------------------------------------------------------------- /.github/scripts/setenv.ps1: -------------------------------------------------------------------------------- 1 | $_VsInstallerDir = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\"; 2 | if (Test-Path "$_VsInstallerDir\vswhere.exe") { 3 | $env:PATH += ";$_VsInstallerDir"; 4 | $installPath = vswhere -latest -property installationPath; 5 | Import-Module (Join-Path $installPath "Common7\Tools\Microsoft.VisualStudio.DevShell.dll") -Force; 6 | Enter-VsDevShell -VsInstallPath $installPath -SkipAutomaticLocation; 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/compile.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: windows-latest 8 | env: 9 | MSYS2_PATH_TYPE: inherit 10 | defaults: 11 | run: 12 | shell: pwsh 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: msys2/setup-msys2@v2 17 | with: 18 | update: false 19 | release: false 20 | install: >- 21 | ninja 22 | 23 | - name: Build 24 | run: | 25 | .github/scripts/setenv.ps1 26 | ninja -C build shim.exe 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 0" # weekly 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - 'version' 12 | 13 | jobs: 14 | build: 15 | runs-on: windows-latest 16 | env: 17 | MSYS2_PATH_TYPE: inherit 18 | defaults: 19 | run: 20 | shell: msys2 {0} 21 | 22 | steps: 23 | - uses: msys2/setup-msys2@v2 24 | with: 25 | update: false 26 | release: false 27 | install: >- 28 | zip 29 | ninja 30 | - uses: actions/checkout@v4 31 | 32 | - name: Get latest release 33 | continue-on-error: true 34 | id: cur_release 35 | uses: pozetroninc/github-action-get-latest-release@master 36 | with: 37 | repository: ${{ github.repository }} 38 | excludes: draft 39 | 40 | - name: Get version 41 | run: | 42 | base_ver=$(cat version) 43 | sha=${GITHUB_SHA::7} 44 | prerelease=$([[ "${{ github.event_name }}" == "schedule" || "${{ github.event_name }}" == "workflow_dispatch" ]] && echo true || echo false) 45 | version=$([[ $prerelease == true ]] && echo ${base_ver}.$sha || echo ${base_ver}) 46 | cur_ver=$([[ ${{ steps.cur_release.outcome }} == failure ]] && echo v0 || echo ${{ steps.cur_release.outputs.release }}) 47 | [[ v$version == $cur_ver ]] && exit 1 48 | echo "version=$version" >> $GITHUB_ENV 49 | echo "prerelease=$prerelease" >> $GITHUB_ENV 50 | 51 | - name: Setup environment 52 | shell: pwsh 53 | run: | 54 | .github/scripts/setenv.ps1 55 | echo $env:PATH | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 56 | 57 | - name: Build 58 | run: ninja -C build 59 | 60 | - name: Create release 61 | uses: ncipollo/release-action@v1 62 | with: 63 | tag: v${{ env.version }} 64 | name: ${{ env.version }} 65 | draft: false 66 | prerelease: ${{ env.prerelease }} 67 | artifactErrorsFailBuild: true 68 | artifacts: build/shimexe.zip 69 | artifactContentType: application/zip 70 | token: ${{ secrets.GITHUB_TOKEN }} 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | *.exe 3 | *.obj 4 | *.pdb 5 | *.ilk 6 | *.o 7 | checksum.sha* 8 | obj/ 9 | bin/ 10 | archive/ 11 | out/ 12 | .zig-cache/ 13 | zig-out/ 14 | .vscode/ 15 | *.exe 16 | *.obj 17 | *.pdb 18 | *.ilk 19 | *.o 20 | checksum.sha* 21 | obj/ 22 | bin/ 23 | archive/ 24 | out/ 25 | .zig-cache/ 26 | zig-out/ 27 | 28 | # User-specific files 29 | *.rsuser 30 | *.suo 31 | *.user 32 | *.userosscache 33 | *.sln.docstates 34 | 35 | # Build results 36 | [Dd]ebug/ 37 | [Dd]ebugPublic/ 38 | [Rr]elease/ 39 | [Rr]eleases/ 40 | x64/ 41 | x86/ 42 | [Ww][Ii][Nn]32/ 43 | [Aa][Rr][Mm]/ 44 | [Aa][Rr][Mm]64/ 45 | bld/ 46 | [Bb]in/ 47 | [Oo]bj/ 48 | [Ll]og/ 49 | [Ll]ogs/ 50 | 51 | # Visual Studio cache/options directory 52 | .vs/ 53 | 54 | # Visual C++ cache files 55 | ipch/ 56 | *.aps 57 | *.ncb 58 | *.opendb 59 | *.opensdf 60 | *.sdf 61 | *.cachefile 62 | *.VC.db 63 | *.VC.VC.opendb 64 | 65 | # Visual Studio profiler 66 | *.psess 67 | *.vsp 68 | *.vspx 69 | *.sap 70 | 71 | # Visual Studio Trace Files 72 | *.e2e -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Grégoire Geis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /LICENSE-UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. 4 | 5 | In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and 6 | successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | For more information, please refer to 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=clang++.exe 2 | CFLAGS=-std=c++17 -m32 3 | 4 | ODIR = obj 5 | BDIR = bin 6 | ADIR = archive 7 | 8 | TARGET = $(BDIR)/shim.exe 9 | OBJ = shim.o 10 | OBJS = $(patsubst %,$(ODIR)/%,$(OBJ)) 11 | 12 | $(TARGET): $(OBJS) | $(BDIR) 13 | $(CC) -o $(TARGET) $^ $(CFLAGS) -Ofast -static 14 | sha256sum $(TARGET) > $(BDIR)/checksum.sha256 15 | sha512sum $(TARGET) > $(BDIR)/checksum.sha512 16 | 17 | $(ODIR)/%.o: %.cpp | $(ODIR) 18 | $(CC) -c -o $@ $< $(CFLAGS) -Ofast -g 19 | 20 | $(ODIR): 21 | mkdir -p $(ODIR) 22 | 23 | $(BDIR): 24 | mkdir -p $(BDIR) 25 | 26 | .PHONY: clean debug zip 27 | 28 | clean: 29 | rm -f $(ODIR)/*.* 30 | rm -f $(BDIR)/*.* 31 | 32 | debug: $(OBJS) | $(BDIR) 33 | $(CC) -o $(BDIR)/shim.exe $^ $(CFLAGS) -g 34 | 35 | $(ADIR): 36 | mkdir -p $(ADIR) 37 | 38 | $(ADIR)/shimexe.zip: $(TARGET) | $(ADIR) 39 | cd $(ADIR) && zip -j -9 shimexe.zip ../$(BDIR)/*.* 40 | 41 | zip: $(ADIR)/shimexe.zip 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Scoop](https://github.com/lukesampson/scoop), although very useful and nice to use, 2 | uses a (subjectively) terrible [`shim.exe`](https://github.com/lukesampson/scoop/blob/master/supporting/shimexe/shim.cs) 3 | file to redirect commands from `scoop\shims\app.exe` to `scoop\apps\app\current\app.exe`, because: 4 | 1. [It's made in C#](https://github.com/lukesampson/scoop/tree/master/supporting/shimexe), 5 | and thus requires an instantiation of a .NET command line app everytime it is started, 6 | which can make a command run much slower than if it had been ran directly; 7 | 2. [It](https://github.com/lukesampson/scoop/issues/2339) [does](https://github.com/lukesampson/scoop/issues/1896) 8 | [not](https://github.com/felixse/FluentTerminal/issues/221) handle Ctrl+C and other 9 | signals correctly, which can be quite infuriating. 10 | 11 | The last issue making interaction with REPLs and long-running apps practically impossible, 12 | and having never been fixed, I set out to improve them with this repository. 13 | 14 | [`shim.cpp`](./shim.cpp) is: 15 | - **Faster**, because it does not use the .NET Framework, and parses the `.shim` file in a simpler way. 16 | - **More efficient**, because by the time the target of the shim is started, all allocated memory will have been freed. 17 | - And more importantly, it **works better**: 18 | - Signals originating from pressing `Ctrl+C` are ignored, and therefore handled directly by the spawned child. 19 | Your processes and REPLs will no longer close when pressing `Ctrl+C`. 20 | - Children are automatically killed when the shim process is killed. No more orphaned processes and weird behaviors. 21 | 22 | > **Note**: This project is not affiliated with [Scoop](https://github.com/lukesampson/scoop). 23 | 24 | ## Installation 25 | 26 | - Building: 27 | - Build by `clang++`: `Makefile` is supported 28 | - Build by using `ninja`: `ninja -C build shim.exe` 29 | - Build by using `msbuild`: `msbuild /p:Configuration=Release .\build\shim.vcxproj` 30 | 31 | - Replace any `.exe` in `scoop\shims` by `shim.exe`. 32 | 33 | An additional script, `repshims.bat`, is provided. It will replace all `.exe`s in the user's Scoop directory 34 | by `shim.exe`. 35 | 36 | 37 | ## License 38 | 39 | `SPDX-License-Identifier: MIT OR Unlicense` 40 | -------------------------------------------------------------------------------- /build/build.ninja: -------------------------------------------------------------------------------- 1 | cc = clang++.exe 2 | cflags = -std=c++20 -m32 3 | 4 | rule cc 5 | command = $cc -c -o $out $in $cflags -O2 -g 6 | 7 | rule link 8 | command = $cc -o $out $cflags -O2 -static -Wl,/manifest:embed -Wl,/manifestinput:$in 9 | 10 | rule link-debug 11 | command = $cc -o $out $cflags -O2 -static -fuse-ld=lld -g -Wl,/manifest:embed -Wl,/manifestinput:$in 12 | 13 | rule mkdir 14 | command = mkdir -p $in 15 | 16 | rule sha256sum 17 | command = sha256sum $in > $out 18 | 19 | rule sha512sum 20 | command = sha512sum $in > $out 21 | 22 | rule zip 23 | command = zip -j -9 $out $in 24 | 25 | build shim.o: cc ../shim.cpp 26 | build shim.exe: link ../shim.manifest shim.o 27 | build shimd.exe: link-debug ../shim.manifest shim.o 28 | build checksum.sha256: sha256sum shim.exe 29 | build checksum.sha512: sha512sum shim.exe 30 | build shimexe.zip: zip shim.exe checksum.sha256 checksum.sha512 31 | 32 | default shimexe.zip 33 | -------------------------------------------------------------------------------- /build/build.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | #Requires -Version 7 3 | 4 | <# 5 | .SYNOPSIS 6 | Build shim.exe using Zig. 7 | .PARAMETER BuildMode 8 | The build mode. Valid values are Debug, ReleaseSafe, ReleaseFast, ReleaseSmall 9 | Default is ReleaseSmall 10 | .PARAMETER Target 11 | The target architecture. Valid values are x86-windows-gnu, x86_64-windows-gnu, aarch64-windows-gnu 12 | Default is undefined (all valid targets) 13 | .PARAMETER Zip 14 | Generate checksums and pack the artifacts into a zip file for distribution 15 | #> 16 | param( 17 | [ValidateSet('Debug', 'ReleaseSafe', 'ReleaseFast', 'ReleaseSmall')] 18 | [string]$BuildMode = "ReleaseSmall", 19 | [ValidateSet('x86-windows-gnu', 'x86_64-windows-gnu', 'aarch64-windows-gnu')] 20 | [string]$Target, 21 | [switch]$Zip = $false 22 | ) 23 | 24 | $oldErrorActionPreference = $ErrorActionPreference 25 | $ErrorActionPreference = "Stop" 26 | 27 | if (-not [bool](Get-Command zig -ErrorAction SilentlyContinue)) { 28 | Write-Host "Zig is not installed. Please install Zig before running this script." -ForegroundColor Yellow 29 | exit 1 30 | } 31 | 32 | Remove-Item -Path "$PSScriptRoot\zig-out" -Recurse -Force -ErrorAction SilentlyContinue 33 | 34 | Push-Location $PSScriptRoot 35 | 36 | if (-not $Target -or $Target -eq 'x86-windows-gnu') { 37 | Write-Host "Build shim.exe for x86-windows-gnu target..." -ForegroundColor Cyan 38 | Start-Process -FilePath "zig" -ArgumentList "build -Dtarget=x86-windows-gnu -Doptimize=$BuildMode" -Wait -NoNewWindow 39 | Rename-Item -Path "$PSScriptRoot\zig-out\bin\shim.exe" -NewName "$PSScriptRoot\zig-out\bin\shim-ia32.exe" 40 | } 41 | 42 | if (-not $Target -or $Target -eq 'x86_64-windows-gnu') { 43 | Write-Host "Build shim.exe for x86_64-windows-gnu target..." -ForegroundColor Cyan 44 | Start-Process -FilePath "zig" -ArgumentList "build -Dtarget=x86_64-windows-gnu -Doptimize=$BuildMode" -Wait -NoNewWindow 45 | Rename-Item -Path "$PSScriptRoot\zig-out\bin\shim.exe" -NewName "$PSScriptRoot\zig-out\bin\shim-amd64.exe" 46 | } 47 | 48 | if (-not $Target -or $Target -eq 'aarch64-windows-gnu') { 49 | Write-Host "Build shim.exe for aarch64-windows-gnu target..." -ForegroundColor Cyan 50 | Start-Process -FilePath "zig" -ArgumentList "build -Dtarget=aarch64-windows-gnu -Doptimize=$BuildMode" -Wait -NoNewWindow 51 | Rename-Item -Path "$PSScriptRoot\zig-out\bin\shim.exe" -NewName "$PSScriptRoot\zig-out\bin\shim-aarch64.exe" 52 | } 53 | 54 | if ($Zip) { 55 | Write-Host "Generate checksums..." -ForegroundColor Cyan 56 | 57 | # shim-ia32.exe 58 | if (-not $Target -or $Target -eq 'x86-windows-gnu') { 59 | $sha256 = (Get-FileHash "$PSScriptRoot\zig-out\bin\shim-ia32.exe" -Algorithm SHA256).Hash.ToLower() 60 | "$sha256 shim-ia32.exe" | Out-File "$PSScriptRoot\zig-out\bin\shim-ia32.exe.sha256" 61 | $sha512 = (Get-FileHash "$PSScriptRoot\zig-out\bin\shim-ia32.exe" -Algorithm SHA512).Hash.ToLower() 62 | "$sha512 shim-ia32.exe" | Out-File "$PSScriptRoot\zig-out\bin\shim-ia32.exe.sha512" 63 | } 64 | 65 | # shim-amd64.exe 66 | if (-not $Target -or $Target -eq 'x86_64-windows-gnu') { 67 | $sha256 = (Get-FileHash "$PSScriptRoot\zig-out\bin\shim-amd64.exe" -Algorithm SHA256).Hash.ToLower() 68 | "$sha256 shim-amd64.exe" | Out-File "$PSScriptRoot\zig-out\bin\shim-amd64.exe.sha256" 69 | $sha512 = (Get-FileHash "$PSScriptRoot\zig-out\bin\shim-amd64.exe" -Algorithm SHA512).Hash.ToLower() 70 | "$sha512 shim-amd64.exe" | Out-File "$PSScriptRoot\zig-out\bin\shim-amd64.exe.sha512" 71 | } 72 | 73 | # shim-aarch64.exe 74 | if (-not $Target -or $Target -eq 'aarch64-windows-gnu') { 75 | $sha256 = (Get-FileHash "$PSScriptRoot\zig-out\bin\shim-aarch64.exe" -Algorithm SHA256).Hash.ToLower() 76 | "$sha256 shim-aarch64.exe" | Out-File "$PSScriptRoot\zig-out\bin\shim-aarch64.exe.sha256" 77 | $sha512 = (Get-FileHash "$PSScriptRoot\zig-out\bin\shim-aarch64.exe" -Algorithm SHA512).Hash.ToLower() 78 | "$sha512 shim-aarch64.exe" | Out-File "$PSScriptRoot\zig-out\bin\shim-aarch64.exe.sha512" 79 | } 80 | 81 | Write-Host "Packaging..." -ForegroundColor Cyan 82 | 83 | $version = (Get-Content "$PSScriptRoot\..\version").Trim() 84 | Compress-Archive -Path "$PSScriptRoot\zig-out\bin\shim-*" -DestinationPath "$PSScriptRoot\zig-out\shimexe-$version.zip" 85 | 86 | $sha256 = (Get-FileHash "$PSScriptRoot\zig-out\shimexe-$version.zip" -Algorithm SHA256).Hash.ToLower() 87 | "$sha256 shimexe-$version.zip" | Out-File "$PSScriptRoot\zig-out\shimexe-$version.zip.sha256" 88 | } 89 | 90 | Write-Host "Artifacts available in $PSScriptRoot\zig-out" -ForegroundColor Green 91 | 92 | Pop-Location 93 | 94 | $ErrorActionPreference = $oldErrorActionPreference 95 | -------------------------------------------------------------------------------- /build/build.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const CrossTarget = std.Target.Query; 4 | 5 | // Usage: 6 | // zig build -Dtarget= -Doptimize= 7 | // Supported targets: 8 | // x86-windows-gnu 9 | // x86-windows-msvc 10 | // x86_64-windows-gnu 11 | // x86_64-windows-msvc 12 | // aarch64-windows-gnu 13 | // aarch64-windows-msvc 14 | 15 | const required_version = std.SemanticVersion.parse("0.13.0") catch unreachable; 16 | const compatible = builtin.zig_version.order(required_version) == .gt; 17 | 18 | pub fn build(b: *std.Build) void { 19 | if (!compatible) { 20 | std.log.err("Unsupported Zig compiler version", .{}); 21 | return; 22 | } 23 | 24 | const optimize = b.standardOptimizeOption(.{}); 25 | const target = b.standardTargetOptions(.{ .default_target = CrossTarget{ 26 | .os_tag = .windows, 27 | .abi = .gnu, 28 | } }); 29 | 30 | if (target.result.os.tag != .windows) { 31 | std.log.err("Non-Windows target is not supported", .{}); 32 | return; 33 | } 34 | 35 | const exe = b.addExecutable(.{ 36 | .name = "shim", 37 | .target = target, 38 | .optimize = optimize, 39 | .win32_manifest = b.path("../shim.manifest"), 40 | }); 41 | 42 | exe.addCSourceFile(.{ .file = b.path("../shim.cpp"), .flags = &.{"-std=c++20"} }); 43 | exe.linkSystemLibrary("shlwapi"); 44 | 45 | if (target.result.abi == .msvc) { 46 | exe.linkLibC(); 47 | } else { 48 | exe.linkLibCpp(); 49 | exe.subsystem = .Console; 50 | // NOTE: This requires a recent Zig version (0.12.0-dev.3493+3661133f9 or later) 51 | exe.mingw_unicode_entry_point = true; 52 | } 53 | 54 | b.installArtifact(exe); 55 | } 56 | -------------------------------------------------------------------------------- /build/shim.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | 14 | 17.0 15 | Win32Proj 16 | shim 17 | 10.0 18 | {93C23795-D457-8055-262A-8D7686A0FB08} 19 | 20 | 21 | Application 22 | v143 23 | Unicode 24 | $(SolutionDir)out\$(Configuration)\bin\ 25 | $(SolutionDir)out\$(Configuration)\obj\ 26 | 27 | 28 | 29 | true 30 | 31 | 32 | false 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Level3 43 | true 44 | true 45 | stdcpplatest 46 | Size 47 | 48 | 49 | Console 50 | true 51 | 52 | 53 | 54 | 55 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 56 | 57 | 58 | 59 | 60 | true 61 | true 62 | Size 63 | true 64 | false 65 | false 66 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 67 | MinSpace 68 | true 69 | MultiThreaded 70 | 71 | 72 | true 73 | true 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /compile_flags.txt: -------------------------------------------------------------------------------- 1 | -std=c++20 2 | -------------------------------------------------------------------------------- /repshims.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | if not defined SCOOP set SCOOP=%USERPROFILE%\scoop 4 | 5 | for %%x in ("%SCOOP%\shims\*.exe") do ( 6 | echo Replacing %%x by new shim. 7 | del "%%~x" 8 | copy "%~dp0"\bin\shim.exe "%%~x" 9 | ) 10 | 11 | if not defined SCOOP_GLOBAL set SCOOP_GLOBAL=%ProgramData%\scoop 12 | 13 | for %%x in ("%SCOOP_GLOBAL%\shims\*.exe") do ( 14 | echo Replacing %%x by new shim. 15 | del "%%~x" 16 | copy "%~dp0"\bin\shim.exe "%%~x" 17 | ) 18 | -------------------------------------------------------------------------------- /shim.cpp: -------------------------------------------------------------------------------- 1 | #ifdef _MSC_VER 2 | #include 3 | #endif 4 | #pragma comment(lib, "SHLWAPI.LIB") 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifndef ERROR_ELEVATION_REQUIRED 17 | #define ERROR_ELEVATION_REQUIRED 740 18 | #endif 19 | 20 | using namespace std::string_view_literals; 21 | 22 | BOOL WINAPI CtrlHandler(DWORD ctrlType) 23 | { 24 | switch (ctrlType) 25 | { 26 | // Ignore all events, and let the child process 27 | // handle them. 28 | case CTRL_C_EVENT: 29 | case CTRL_CLOSE_EVENT: 30 | case CTRL_LOGOFF_EVENT: 31 | case CTRL_BREAK_EVENT: 32 | case CTRL_SHUTDOWN_EVENT: 33 | return TRUE; 34 | 35 | default: 36 | return FALSE; 37 | } 38 | } 39 | 40 | struct HandleDeleter 41 | { 42 | typedef HANDLE pointer; 43 | void operator() (HANDLE handle) 44 | { 45 | if (handle) 46 | { 47 | CloseHandle(handle); 48 | } 49 | } 50 | }; 51 | 52 | namespace std 53 | { 54 | typedef unique_ptr unique_handle; 55 | typedef optional wstring_p; 56 | } 57 | 58 | struct ShimInfo 59 | { 60 | std::wstring_p path; 61 | std::wstring_p args; 62 | }; 63 | 64 | std::wstring_view GetDirectory(std::wstring_view exe) 65 | { 66 | auto pos = exe.find_last_of(L"\\/"); 67 | return exe.substr(0, pos); 68 | } 69 | 70 | std::wstring_p NormalizeArgs(std::wstring_p& args, std::wstring_view curDir) 71 | { 72 | static constexpr auto s_dirPlaceHolder = L"%~dp0"sv; 73 | if (!args) 74 | { 75 | return args; 76 | } 77 | 78 | auto pos = args->find(s_dirPlaceHolder); 79 | if (pos != std::wstring::npos) 80 | { 81 | args->replace(pos, s_dirPlaceHolder.size(), curDir.data(), curDir.size()); 82 | } 83 | 84 | return args; 85 | } 86 | 87 | ShimInfo GetShimInfo() 88 | { 89 | // Find filename of current executable. 90 | wchar_t filename[MAX_PATH + 2]; 91 | const auto filenameSize = GetModuleFileNameW(nullptr, filename, MAX_PATH); 92 | 93 | if (filenameSize >= MAX_PATH) 94 | { 95 | fprintf(stderr, "Shim: The filename of the program is too long to handle.\n"); 96 | return {std::nullopt, std::nullopt}; 97 | } 98 | 99 | // Use filename of current executable to find .shim 100 | wmemcpy(filename + filenameSize - 3, L"shim", 4U); 101 | filename[filenameSize + 1] = L'\0'; 102 | FILE* fp = nullptr; 103 | 104 | if (_wfopen_s(&fp, filename, L"r,ccs=UTF-8") != 0) 105 | { 106 | fprintf(stderr, "Cannot open shim file for read.\n"); 107 | return {std::nullopt, std::nullopt}; 108 | } 109 | 110 | std::unique_ptr shimFile(fp, &fclose); 111 | 112 | // Read shim 113 | wchar_t linebuf[1<<14]; 114 | std::wstring_p path; 115 | std::wstring_p args; 116 | while (true) 117 | { 118 | if (!fgetws(linebuf, ARRAYSIZE(linebuf), shimFile.get())) 119 | { 120 | break; 121 | } 122 | 123 | std::wstring_view line(linebuf); 124 | 125 | if ((line.size() < 7) || (line.substr(4, 3) != L" = ")) 126 | { 127 | continue; 128 | } 129 | 130 | if (line.substr(0, 4) == L"path") 131 | { 132 | std::wstring_view line_substr = line.substr(7); 133 | if (line_substr.find(L" ") != std::wstring_view::npos && line_substr.front() != L'"') 134 | { 135 | path.emplace(L"\""); 136 | auto& path_value = path.value(); 137 | path_value.append(line_substr.data(), line_substr.size() - (line.back() == L'\n' ? 1 : 0)); 138 | path_value.push_back(L'"'); 139 | } 140 | else 141 | { 142 | path.emplace(line_substr.data(), line_substr.size() - (line.back() == L'\n' ? 1 : 0)); 143 | } 144 | 145 | continue; 146 | } 147 | 148 | if (line.substr(0, 4) == L"args") 149 | { 150 | args.emplace(line.data() + 7, line.size() - 7 - (line.back() == L'\n' ? 1 : 0)); 151 | continue; 152 | } 153 | } 154 | 155 | return {path, NormalizeArgs(args, GetDirectory(filename))}; 156 | } 157 | 158 | std::tuple MakeProcess(ShimInfo const& info) 159 | { 160 | // Start subprocess 161 | STARTUPINFOW si = {}; 162 | PROCESS_INFORMATION pi = {}; 163 | 164 | auto&& [path, args] = info; 165 | std::vector cmd(path->size() + args->size() + 2); 166 | wmemcpy(cmd.data(), path->c_str(), path->size()); 167 | cmd[path->size()] = L' '; 168 | wmemcpy(cmd.data() + path->size() + 1, args->c_str(), args->size()); 169 | cmd[path->size() + 1 + args->size()] = L'\0'; 170 | 171 | std::unique_handle threadHandle; 172 | std::unique_handle processHandle; 173 | 174 | GetStartupInfoW(&si); 175 | 176 | if (CreateProcessW(nullptr, cmd.data(), nullptr, nullptr, TRUE, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) 177 | { 178 | threadHandle.reset(pi.hThread); 179 | processHandle.reset(pi.hProcess); 180 | 181 | ResumeThread(threadHandle.get()); 182 | } 183 | else 184 | { 185 | if (GetLastError() == ERROR_ELEVATION_REQUIRED) 186 | { 187 | // We must elevate the process, which is (basically) impossible with 188 | // CreateProcess, and therefore we fallback to ShellExecuteEx, 189 | // which CAN create elevated processes, at the cost of opening a new separate 190 | // window. 191 | // Theorically, this could be fixed (or rather, worked around) using pipes 192 | // and IPC, but... this is a question for another day. 193 | SHELLEXECUTEINFOW sei = {}; 194 | 195 | sei.cbSize = sizeof(SHELLEXECUTEINFOW); 196 | sei.fMask = SEE_MASK_NOCLOSEPROCESS; 197 | sei.lpFile = path->c_str(); 198 | sei.lpParameters = args->c_str(); 199 | sei.nShow = SW_SHOW; 200 | 201 | if (!ShellExecuteExW(&sei)) 202 | { 203 | fprintf(stderr, "Shim: Unable to create elevated process: error %li.", GetLastError()); 204 | return {std::move(processHandle), std::move(threadHandle)}; 205 | } 206 | 207 | processHandle.reset(sei.hProcess); 208 | } 209 | else 210 | { 211 | fprintf(stderr, "Shim: Could not create process with command '%ls'.\n", cmd.data()); 212 | return {std::move(processHandle), std::move(threadHandle)}; 213 | } 214 | } 215 | 216 | // Ignore Ctrl-C and other signals 217 | if (!SetConsoleCtrlHandler(CtrlHandler, TRUE)) 218 | { 219 | fprintf(stderr, "Shim: Could not set control handler; Ctrl-C behavior may be invalid.\n"); 220 | } 221 | 222 | return {std::move(processHandle), std::move(threadHandle)}; 223 | } 224 | 225 | int wmain(int argc, wchar_t* argv[]) 226 | { 227 | auto [path, args] = GetShimInfo(); 228 | 229 | if (!path) 230 | { 231 | fprintf(stderr, "Could not read shim file.\n"); 232 | return 1; 233 | } 234 | 235 | if (!args) 236 | { 237 | args.emplace(); 238 | } 239 | 240 | auto cmd = GetCommandLineW(); 241 | if (cmd[0] == L'\"') 242 | { 243 | args->append(cmd + wcslen(argv[0]) + 2); 244 | } 245 | else 246 | { 247 | args->append(cmd + wcslen(argv[0])); 248 | } 249 | 250 | // Find out if the target program is a console app 251 | 252 | wchar_t unquotedPath[MAX_PATH] = {}; 253 | wmemcpy(unquotedPath, path->c_str(), path->length()); 254 | PathUnquoteSpacesW(unquotedPath); 255 | SHFILEINFOW sfi = {}; 256 | const auto ret = SHGetFileInfoW(unquotedPath, -1, &sfi, sizeof(sfi), SHGFI_EXETYPE); 257 | 258 | if (ret == 0) 259 | { 260 | fprintf(stderr, "Shim: Could not determine if target is a GUI app. Assuming console.\n"); 261 | } 262 | 263 | const auto isWindowsApp = HIWORD(ret) != 0; 264 | 265 | if (isWindowsApp) 266 | { 267 | // Unfortunately, this technique will still show a window for a fraction of time, 268 | // but there's just no workaround. 269 | FreeConsole(); 270 | } 271 | 272 | // Create job object, which can be attached to child processes 273 | // to make sure they terminate when the parent terminates as well. 274 | std::unique_handle jobHandle(CreateJobObject(nullptr, nullptr)); 275 | JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli = {}; 276 | 277 | jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; 278 | SetInformationJobObject(jobHandle.get(), JobObjectExtendedLimitInformation, &jeli, sizeof(jeli)); 279 | 280 | auto [processHandle, threadHandle] = MakeProcess({path, args}); 281 | if (processHandle) 282 | { 283 | AssignProcessToJobObject(jobHandle.get(), processHandle.get()); 284 | 285 | // Wait till end of process 286 | WaitForSingleObject(processHandle.get(), INFINITE); 287 | 288 | DWORD exitCode = 0; 289 | GetExitCodeProcess(processHandle.get(), &exitCode); 290 | 291 | return exitCode; 292 | } 293 | 294 | return processHandle ? 0 : 1; 295 | } 296 | -------------------------------------------------------------------------------- /shim.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | --------------------------------------------------------------------------------