├── .github ├── renovate.json └── workflows │ └── ci.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── internal ├── localcheck │ ├── arch.go │ ├── arch_test.go │ ├── file.go │ ├── file_test.go │ ├── local.go │ └── local_test.go ├── macapp │ ├── macapp.go │ └── macapp_test.go ├── output │ ├── output.go │ └── output_test.go └── remotecheck │ ├── err.go │ ├── remote.go │ ├── remote_test.go │ ├── support.go │ └── support_test.go ├── main.go └── test └── data ├── bash.sh ├── env_bash.sh ├── env_invalid.sh ├── example_fat.app └── Contents │ ├── Info.plist │ └── MacOS │ └── example ├── example_macho.app └── Contents │ ├── Info.plist │ └── MacOS │ └── example ├── invalid_interpreter.app └── Contents │ ├── Info.plist │ └── MacOS │ └── run.sh ├── invalid_plist.app └── Contents │ └── Info.plist ├── invalid_shebang.sh ├── sh_app.app └── Contents │ ├── Info.plist │ └── MacOS │ └── run.sh └── unknown_type.app └── Contents └── Info.plist /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":disableDependencyDashboard" 5 | ], 6 | "labels": ["dependencies"], 7 | "packageRules": [ 8 | { 9 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 10 | "automerge": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | go-version: ['1.20', '1.19' ] 10 | os: [macos-latest, ubuntu-latest] 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test with coverage 28 | run: go test -v ./... -cover -coverprofile=coverage.txt -covermode=atomic 29 | 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v3 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Go ### 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | ### Go Patch ### 20 | /vendor/ 21 | /Godeps/ 22 | 23 | ### vscode ### 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | *.code-workspace 30 | 31 | ### Dist Releases ### 32 | dist/ 33 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod tidy 6 | # - go generate ./... 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - darwin 12 | archives: 13 | - replacements: 14 | darwin: Darwin 15 | linux: Linux 16 | windows: Windows 17 | 386: i386 18 | amd64: x86_64 19 | checksum: 20 | name_template: 'checksums.txt' 21 | snapshot: 22 | name_template: "{{ .Tag }}-next" 23 | changelog: 24 | sort: asc 25 | filters: 26 | exclude: 27 | - '^docs:' 28 | - '^test:' 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Chongyi Zheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apple Silicon Check 2 | 3 | [![Actions Status](https://github.com/harryzcy/ascheck/workflows/CI/badge.svg)](https://github.com/harryzcy/ascheck/actions) 4 | [![codecov](https://codecov.io/gh/harryzcy/ascheck/branch/main/graph/badge.svg)](https://codecov.io/gh/harryzcy/ascheck) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/harryzcy/ascheck)](https://goreportcard.com/report/github.com/harryzcy/ascheck) 6 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) 7 | 8 | A CLI tool that bulk-checks your apps for the Apple Silicon support. 9 | 10 | --- 11 | 12 | ## Table of Contents 13 | 14 | - [Installation](#installation) 15 | - [Homebrew tap](#homebrew-tap) 16 | - [go install](#go-install) 17 | - [Compile from source](#compile-from-source) 18 | - [Example Usage](#example-usage) 19 | - [Show help](#show-help) 20 | - [Run](#run) 21 | - [Output](#output) 22 | 23 | --- 24 | 25 | ## Installation 26 | 27 | ### Homebrew tap 28 | 29 | ```shell 30 | brew tap harryzcy/ascheck 31 | brew install ascheck 32 | ``` 33 | 34 | ### go install 35 | 36 | ```shell 37 | go install github.com/harryzcy/ascheck 38 | ``` 39 | 40 | ### Compile from source 41 | 42 | #### clone 43 | 44 | ```shell 45 | git clone https://github.com/harryzheng/ascheck 46 | cd ascheck 47 | ``` 48 | 49 | #### get the dependencies 50 | 51 | ```shell 52 | go mod tidy 53 | ``` 54 | 55 | #### build 56 | 57 | ```shell 58 | go build -o ascheck . 59 | ``` 60 | 61 | ## Example Usage 62 | 63 | ### Show help 64 | 65 | ```shell 66 | ascheck -h 67 | ``` 68 | 69 | ### Run 70 | 71 | ```shell 72 | ascheck 73 | ``` 74 | 75 | ### Output 76 | 77 | The output will show: 78 | 79 | ```shell 80 | NAME CURRENT ARCHITECTURES ARM SUPPORT 81 | ------------------------------------------------ 82 | App Store Intel 64 Supported 83 | Automator Intel 64 Supported 84 | ... 85 | ``` 86 | 87 | - NAME: name of the app 88 | - CURRENT ARCHITECTURES: the architecture of the currently installed version 89 | - ARM SUPPORT: the arm support information on [Does it Arm](https://github.com/ThatGuySam/doesitarm) 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/harryzcy/ascheck 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/olekukonko/tablewriter v0.0.5 7 | github.com/stretchr/testify v1.8.2 8 | github.com/urfave/cli/v2 v2.25.2 9 | howett.net/plist v1.0.0 10 | ) 11 | 12 | require ( 13 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/mattn/go-runewidth v0.0.14 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/rivo/uniseg v0.4.4 // indirect 18 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 19 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 7 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 8 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 9 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 10 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 11 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 15 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 16 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 17 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 18 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 25 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 26 | github.com/urfave/cli/v2 v2.25.0 h1:ykdZKuQey2zq0yin/l7JOm9Mh+pg72ngYMeB0ABn6q8= 27 | github.com/urfave/cli/v2 v2.25.0/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 28 | github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= 29 | github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 30 | github.com/urfave/cli/v2 v2.25.2 h1:rgeK7wmjwH+d3DqXDDSV20GZAvNzmzu/VEsg1om3Qwg= 31 | github.com/urfave/cli/v2 v2.25.2/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 32 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 33 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 37 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 39 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= 41 | howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 42 | -------------------------------------------------------------------------------- /internal/localcheck/arch.go: -------------------------------------------------------------------------------- 1 | package localcheck 2 | 3 | import ( 4 | "debug/macho" 5 | "strings" 6 | ) 7 | 8 | // Architectures represents all supported architecture of an app. 9 | type Architectures struct { 10 | Intel uint 11 | Arm uint 12 | PowerPC uint 13 | } 14 | 15 | // Load loads the architectures from macho.Cpu. 16 | func (arch *Architectures) Load(cpu macho.Cpu) { 17 | switch cpu { 18 | case macho.Cpu386: 19 | arch.Intel |= 0b01 20 | case macho.CpuAmd64: 21 | arch.Intel |= 0b10 22 | case macho.CpuArm: 23 | arch.Arm |= 0b01 24 | case macho.CpuArm64: 25 | arch.Arm |= 0b10 26 | case macho.CpuPpc: 27 | arch.PowerPC |= 0b01 28 | case macho.CpuPpc64: 29 | arch.PowerPC |= 0b10 30 | } 31 | } 32 | 33 | // LoadFromFat loads the architectures from []macho.FatArch. 34 | func (arch *Architectures) LoadFromFat(src []macho.FatArch) { 35 | for _, fat := range src { 36 | arch.Load(fat.Cpu) 37 | } 38 | } 39 | 40 | // String returns the architecture in string format. 41 | func (arch *Architectures) String() string { 42 | var list []string 43 | 44 | if arch.PowerPC > 0 { 45 | list = append(list, "PowerPC "+getBitString(arch.PowerPC)) 46 | } 47 | if arch.Intel > 0 { 48 | list = append(list, "Intel "+getBitString(arch.Intel)) 49 | } 50 | if arch.Arm > 0 { 51 | list = append(list, "Arm "+getBitString(arch.Arm)) 52 | } 53 | 54 | if len(list) > 0 { 55 | return strings.Join(list, ", ") 56 | } 57 | 58 | return "Unknown" 59 | } 60 | 61 | func getBitString(mask uint) string { 62 | switch mask { 63 | case 0b11: 64 | return "32/64" 65 | case 0b01: 66 | return "32" 67 | case 0b10: 68 | return "64" 69 | default: 70 | return "" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/localcheck/arch_test.go: -------------------------------------------------------------------------------- 1 | package localcheck 2 | 3 | import ( 4 | "debug/macho" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestArchitecture_Load(t *testing.T) { 11 | tests := []struct { 12 | cpu macho.Cpu 13 | expected Architectures 14 | }{ 15 | {macho.CpuPpc, Architectures{PowerPC: 0b01}}, 16 | {macho.CpuPpc64, Architectures{PowerPC: 0b10}}, 17 | {macho.Cpu386, Architectures{Intel: 0b01}}, 18 | {macho.CpuAmd64, Architectures{Intel: 0b10}}, 19 | {macho.CpuArm, Architectures{Arm: 0b01}}, 20 | {macho.CpuArm64, Architectures{Arm: 0b10}}, 21 | } 22 | 23 | for _, test := range tests { 24 | arch := Architectures{} 25 | assert.Empty(t, arch) 26 | 27 | arch.Load(test.cpu) 28 | assert.Equal(t, test.expected, arch) 29 | } 30 | } 31 | 32 | func TestArchitecture_LoadFat(t *testing.T) { 33 | tests := []struct { 34 | in []macho.FatArch 35 | expected Architectures 36 | }{ 37 | {[]macho.FatArch{ 38 | {FatArchHeader: macho.FatArchHeader{Cpu: macho.CpuAmd64}}, 39 | }, 40 | Architectures{Intel: 0b10}, 41 | }, 42 | {[]macho.FatArch{ 43 | {FatArchHeader: macho.FatArchHeader{Cpu: macho.CpuAmd64}}, 44 | {FatArchHeader: macho.FatArchHeader{Cpu: macho.CpuArm64}}, 45 | }, 46 | Architectures{Intel: 0b10, Arm: 0b10}, 47 | }, 48 | } 49 | 50 | for _, test := range tests { 51 | arch := Architectures{} 52 | assert.Empty(t, arch) 53 | 54 | arch.LoadFromFat(test.in) 55 | assert.Equal(t, test.expected, arch) 56 | } 57 | } 58 | 59 | func TestArchitecture_String(t *testing.T) { 60 | tests := []struct { 61 | arch Architectures 62 | expected string 63 | }{ 64 | {Architectures{PowerPC: 0b01}, "PowerPC 32"}, 65 | {Architectures{PowerPC: 0b10}, "PowerPC 64"}, 66 | {Architectures{PowerPC: 0b11}, "PowerPC 32/64"}, 67 | {Architectures{Intel: 0b01}, "Intel 32"}, 68 | {Architectures{Intel: 0b10}, "Intel 64"}, 69 | {Architectures{Intel: 0b11}, "Intel 32/64"}, 70 | {Architectures{Arm: 0b01}, "Arm 32"}, 71 | {Architectures{Arm: 0b10}, "Arm 64"}, 72 | {Architectures{Arm: 0b11}, "Arm 32/64"}, 73 | 74 | {Architectures{Intel: 0b10, Arm: 0b10}, "Intel 64, Arm 64"}, 75 | {Architectures{Intel: 0b11, Arm: 0b10}, "Intel 32/64, Arm 64"}, 76 | {Architectures{PowerPC: 0b11, Arm: 0b11}, "PowerPC 32/64, Arm 32/64"}, 77 | 78 | {Architectures{}, "Unknown"}, 79 | } 80 | 81 | for _, test := range tests { 82 | actual := test.arch.String() 83 | assert.Equal(t, test.expected, actual) 84 | } 85 | } 86 | 87 | func TestGetBitString_EdgeCase(t *testing.T) { 88 | assert.Empty(t, getBitString(0)) 89 | } 90 | -------------------------------------------------------------------------------- /internal/localcheck/file.go: -------------------------------------------------------------------------------- 1 | package localcheck 2 | 3 | import ( 4 | "os" 5 | "unicode/utf8" 6 | ) 7 | 8 | // IsText reports whether a significant prefix of s looks like correct UTF-8; 9 | // that is, if it is likely that s is human-readable text. 10 | func IsText(s []byte) bool { 11 | const max = 1024 // at least utf8.UTFMax 12 | if len(s) > max { 13 | s = s[0:max] 14 | } 15 | for i, c := range string(s) { 16 | if i+utf8.UTFMax > len(s) { 17 | // last char may be incomplete - ignore 18 | break 19 | } 20 | if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' { 21 | // decoding error or control character - not a text file 22 | return false 23 | } 24 | } 25 | return true 26 | } 27 | 28 | // IsTextFile reports if a significant chunk of the specified file looks like 29 | // correct UTF-8; that is, if it is likely that the file contains human- 30 | // readable text. 31 | func IsTextFile(filename string) bool { 32 | // read an initial chunk of the file 33 | // and check if it looks like text 34 | f, err := os.Open(filename) 35 | if err != nil { 36 | return false 37 | } 38 | defer f.Close() 39 | 40 | var buf [1024]byte 41 | n, err := f.Read(buf[0:]) 42 | if err != nil { 43 | return false 44 | } 45 | 46 | return IsText(buf[0:n]) 47 | } 48 | -------------------------------------------------------------------------------- /internal/localcheck/file_test.go: -------------------------------------------------------------------------------- 1 | package localcheck 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIsText(t *testing.T) { 11 | tests := []struct { 12 | in []byte 13 | expected bool 14 | }{ 15 | {[]byte("#!/bin/bash\n"), true}, 16 | {[]byte("some string"), true}, 17 | {[]byte("#!/usr/bin/env bash\n"), true}, 18 | {[]byte(strings.Repeat("some string ", 100)), true}, 19 | {[]byte{0x00, 0x01, 0x02, 0x3}, false}, 20 | } 21 | 22 | for _, test := range tests { 23 | actual := IsText(test.in) 24 | assert.Equal(t, test.expected, actual) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/localcheck/local.go: -------------------------------------------------------------------------------- 1 | package localcheck 2 | 3 | import ( 4 | "bufio" 5 | "debug/macho" 6 | "errors" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | 12 | "howett.net/plist" 13 | ) 14 | 15 | type executableDecoded struct { 16 | CFBundleExecutable string 17 | } 18 | 19 | func getExecutableName(path string) (string, error) { 20 | plistFile := filepath.Join(path, "Contents", "Info.plist") 21 | 22 | f, err := os.Open(plistFile) 23 | if err != nil { 24 | return "", err 25 | } 26 | defer f.Close() 27 | 28 | decoder := plist.NewDecoder(f) 29 | var plistDecoded executableDecoded 30 | err = decoder.Decode(&plistDecoded) 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | return plistDecoded.CFBundleExecutable, err 36 | } 37 | 38 | // GetArchitectures returns all supported architecture given the app's path. 39 | func GetArchitectures(path string) (Architectures, error) { 40 | executableName, err := getExecutableName(path) 41 | if err != nil { 42 | return Architectures{}, err 43 | } 44 | 45 | // binary file path 46 | executable := filepath.Join(path, "Contents", "MacOS", executableName) 47 | 48 | return getExecutableArchitectures(executable) 49 | } 50 | 51 | func getExecutableArchitectures(path string) (Architectures, error) { 52 | var ( 53 | arch = Architectures{} 54 | ) 55 | 56 | // file is a Mach-O universal file 57 | fat, err := macho.OpenFat(path) 58 | if err == nil { 59 | arch.LoadFromFat(fat.Arches) 60 | return arch, nil 61 | } 62 | 63 | // file is a Mach-O file 64 | f, err := macho.Open(path) 65 | if err == nil { 66 | arch.Load(f.Cpu) 67 | return arch, nil 68 | } 69 | 70 | // file is a text file 71 | if IsTextFile(path) { 72 | interpreter, ok := getInterpreterPath(path) 73 | if !ok { 74 | return arch, errors.New("unable to get executable path") 75 | } 76 | return getExecutableArchitectures(interpreter) 77 | } 78 | 79 | return arch, errors.New("unknown file type") 80 | } 81 | 82 | func getInterpreterPath(filename string) (path string, ok bool) { 83 | f, err := os.Open(filename) 84 | if err != nil { 85 | return "", false 86 | } 87 | defer f.Close() 88 | 89 | // read the first line of the file; ensure that it starts with Shebang 90 | reader := bufio.NewReader(f) 91 | line, _ := reader.ReadString('\n') 92 | line = strings.TrimSuffix(line, "\n") 93 | if !strings.HasPrefix(line, "#!") { 94 | return "", false 95 | } 96 | 97 | line = line[2:] // skip Shebang 98 | if strings.HasPrefix(line, "/usr/bin/env") { 99 | line = line[13:] // skip logical path 100 | 101 | interpreter := strings.SplitN(line, " ", 2)[0] 102 | path, err := exec.LookPath(interpreter) 103 | if err != nil { 104 | return "", false 105 | } 106 | 107 | return path, true 108 | } 109 | 110 | path = strings.SplitN(line, " ", 2)[0] 111 | 112 | return path, true 113 | } 114 | -------------------------------------------------------------------------------- /internal/localcheck/local_test.go: -------------------------------------------------------------------------------- 1 | package localcheck 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestApplication_GetExecutableName(t *testing.T) { 12 | exec, err := getExecutableName("./../../test/data/sh_app.app") 13 | assert.Nil(t, err) 14 | assert.Equal(t, "run.sh", exec) 15 | } 16 | 17 | func TestApplication_GetArchitectures(t *testing.T) { 18 | arch, err := GetArchitectures("./../../test/data/example_macho.app") 19 | assert.Nil(t, err) 20 | assert.EqualValues(t, 0b01, arch.Intel) 21 | assert.EqualValues(t, 0, arch.PowerPC) 22 | assert.EqualValues(t, 0, arch.Arm) 23 | 24 | arch, err = GetArchitectures("./../../test/data/example_fat.app") 25 | assert.Nil(t, err) 26 | assert.EqualValues(t, 0b10, arch.Intel) 27 | assert.EqualValues(t, 0, arch.PowerPC) 28 | assert.EqualValues(t, 0, arch.Arm) 29 | 30 | arch, err = GetArchitectures("./../../test/data/sh_app.app") 31 | if err == nil { // should pass on macOS 32 | assert.NotEmpty(t, arch) 33 | } else { // would failed on linux 34 | assert.Equal(t, errors.New("unknown file type"), err) 35 | assert.Empty(t, arch) 36 | } 37 | } 38 | 39 | func TestApplication_GetArchitectures_Error(t *testing.T) { 40 | // Invalid path 41 | arch, err := GetArchitectures("./../../test/data/invalid.app") 42 | assert.NotNil(t, err) 43 | assert.True(t, os.IsNotExist(err)) 44 | assert.Empty(t, arch) 45 | 46 | // Invalid plist 47 | arch, err = GetArchitectures("./../../test/data/invalid_plist.app") 48 | assert.NotNil(t, err) 49 | assert.False(t, os.IsNotExist(err)) 50 | assert.Empty(t, arch) 51 | 52 | // Invalid interpreter 53 | arch, err = GetArchitectures("./../../test/data/invalid_interpreter.app") 54 | assert.NotNil(t, err) 55 | assert.Equal(t, errors.New("unable to get executable path"), err) 56 | assert.Empty(t, arch) 57 | 58 | // Unknown file type 59 | arch, err = GetArchitectures("./../../test/data/unknown_type.app") 60 | assert.NotNil(t, err) 61 | assert.Equal(t, errors.New("unknown file type"), err) 62 | assert.Empty(t, arch) 63 | } 64 | 65 | func TestGetInterpreterPath(t *testing.T) { 66 | tests := []struct { 67 | file string 68 | expectedPath []string 69 | expectedOK bool 70 | }{ 71 | {"./../../test/data/bash.sh", []string{"/bin/bash"}, true}, 72 | {"./../../test/data/env_bash.sh", []string{"/bin/bash", "/usr/bin/bash"}, true}, 73 | {"./../../test/data/invalid.sh", []string{""}, false}, 74 | {"./../../test/data/env_invalid.sh", []string{""}, false}, 75 | {"./../../test/data/invalid_shebang.sh", []string{""}, false}, 76 | } 77 | 78 | for _, test := range tests { 79 | path, ok := getInterpreterPath(test.file) 80 | assert.Contains(t, test.expectedPath, path) 81 | assert.Equal(t, test.expectedOK, ok) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/macapp/macapp.go: -------------------------------------------------------------------------------- 1 | package macapp 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/harryzcy/ascheck/internal/localcheck" 10 | "github.com/harryzcy/ascheck/internal/remotecheck" 11 | ) 12 | 13 | var ( 14 | applicationPath []string 15 | ) 16 | 17 | func init() { 18 | usr, _ := user.Current() 19 | userApplication := filepath.Join(usr.HomeDir, "Applications") 20 | 21 | applicationPath = []string{ 22 | "/System/Applications", 23 | "/Applications", 24 | userApplication, 25 | } 26 | } 27 | 28 | // Application represents an installed app. 29 | type Application struct { 30 | // Name shows the app name 31 | Name string 32 | 33 | // Path shows the physical location 34 | Path string 35 | // Architectures represents the architectures of the currently installed version 36 | Architectures localcheck.Architectures 37 | 38 | // Website shows the app's website, empty if unknown 39 | Website string 40 | // ArmSupport shows the Apple Silicon support based on Does It Arm reports 41 | ArmSupport remotecheck.Support 42 | } 43 | 44 | // GetAllApplications returns all applications. 45 | func GetAllApplications(dirs []string) ([]Application, error) { 46 | var ( 47 | applications []Application 48 | ) 49 | 50 | if dirs == nil { 51 | dirs = applicationPath 52 | } 53 | 54 | for _, dir := range dirs { 55 | entries, err := os.ReadDir(dir) 56 | if err != nil { 57 | if os.IsNotExist(err) { 58 | continue 59 | } 60 | return nil, err 61 | } 62 | 63 | for _, entry := range entries { 64 | if strings.HasSuffix(entry.Name(), ".app") { 65 | app := checkApplication(dir, entry) 66 | applications = append(applications, app) 67 | } 68 | } 69 | } 70 | 71 | return applications, nil 72 | } 73 | 74 | func checkApplication(dir string, entry os.DirEntry) Application { 75 | app := Application{ 76 | Name: strings.TrimSuffix(entry.Name(), ".app"), 77 | Path: filepath.Join(dir, entry.Name()), 78 | } 79 | 80 | app.Architectures, _ = localcheck.GetArchitectures(app.Path) 81 | 82 | // mark system apps as natively supported 83 | if strings.HasPrefix(dir, "/System/") { 84 | app.ArmSupport = remotecheck.SupportNative 85 | return app 86 | } 87 | 88 | info, err := remotecheck.GetInfo(app.Name) 89 | if err == nil { 90 | app.Website = info.Website 91 | app.ArmSupport = info.ArmSupport 92 | } 93 | 94 | return app 95 | } 96 | -------------------------------------------------------------------------------- /internal/macapp/macapp_test.go: -------------------------------------------------------------------------------- 1 | package macapp 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetAllApplications(t *testing.T) { 10 | apps, err := GetAllApplications(nil) 11 | assert.Nil(t, err) 12 | assert.IsType(t, []Application{}, apps) 13 | } 14 | -------------------------------------------------------------------------------- /internal/output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "os" 7 | 8 | "github.com/harryzcy/ascheck/internal/macapp" 9 | "github.com/olekukonko/tablewriter" 10 | ) 11 | 12 | var out io.Writer = os.Stdout 13 | 14 | // Table prints application information in table format. 15 | func Table(apps []macapp.Application) { 16 | table := tablewriter.NewWriter(out) 17 | table.SetHeader([]string{"Name", "Current Architectures", "Arm Support"}) 18 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 19 | table.SetCenterSeparator("") 20 | table.SetColumnSeparator("") 21 | table.SetBorder(false) 22 | 23 | for _, app := range apps { 24 | table.Append([]string{app.Name, app.Architectures.String(), app.ArmSupport.String()}) 25 | } 26 | 27 | table.Render() 28 | } 29 | 30 | // JSON prints application information in json format. 31 | func JSON(apps []macapp.Application) { 32 | items := make([]map[string]string, len(apps)) 33 | 34 | for idx, app := range apps { 35 | row := map[string]string{ 36 | "name": app.Name, 37 | "currentArchitectures": app.Architectures.String(), 38 | "armSupport": app.ArmSupport.String(), 39 | } 40 | items[idx] = row 41 | } 42 | 43 | output, _ := json.Marshal(map[string]interface{}{ 44 | "items": items, 45 | }) 46 | output = append(output, byte('\n')) 47 | out.Write(output) 48 | } 49 | -------------------------------------------------------------------------------- /internal/output/output_test.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/harryzcy/ascheck/internal/localcheck" 8 | "github.com/harryzcy/ascheck/internal/macapp" 9 | "github.com/harryzcy/ascheck/internal/remotecheck" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var str = new(strings.Builder) 14 | 15 | func init() { 16 | out = str 17 | } 18 | 19 | func TestTable(t *testing.T) { 20 | apps := []macapp.Application{ 21 | {Name: "a", Architectures: localcheck.Architectures{Intel: 0b10}, ArmSupport: remotecheck.SupportNative}, 22 | {Name: "b", Architectures: localcheck.Architectures{Intel: 0b10}, ArmSupport: remotecheck.SupportNative}, 23 | } 24 | 25 | str.Reset() 26 | 27 | Table(apps) 28 | 29 | assert.Equal(t, ""+ 30 | " NAME CURRENT ARCHITECTURES ARM SUPPORT \n"+ 31 | "--------------------------------------------\n"+ 32 | " a Intel 64 Supported \n"+ 33 | " b Intel 64 Supported \n", 34 | str.String()) 35 | 36 | } 37 | 38 | func TestJSON(t *testing.T) { 39 | apps := []macapp.Application{ 40 | {Name: "a", Architectures: localcheck.Architectures{Intel: 0b10}, ArmSupport: remotecheck.SupportNative}, 41 | } 42 | 43 | str.Reset() 44 | 45 | JSON(apps) 46 | 47 | assert.Equal(t, `{"items":[{"armSupport":"Supported","currentArchitectures":"Intel 64","name":"a"}]}`+"\n", 48 | str.String(), 49 | ) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /internal/remotecheck/err.go: -------------------------------------------------------------------------------- 1 | package remotecheck 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | // ErrNotFound is returned when an app is not found. 9 | ErrNotFound = errors.New("app not found") 10 | ) 11 | -------------------------------------------------------------------------------- /internal/remotecheck/remote.go: -------------------------------------------------------------------------------- 1 | package remotecheck 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "regexp" 7 | ) 8 | 9 | const ( 10 | sourceURL = "https://cdn.jsdelivr.net/gh/ThatGuySam/doesitarm@master/README.md" 11 | ) 12 | 13 | var ( 14 | pattern, _ = regexp.Compile(`\* \[(.*?)\]\((.*?)\) - (✅|✳️|⏹|🚫|🔶)`) 15 | 16 | infoCache map[string]AppInfo = make(map[string]AppInfo) 17 | ) 18 | 19 | // AppInfo contains information of an app obtained from remote sources. 20 | type AppInfo struct { 21 | Website string 22 | ArmSupport Support 23 | } 24 | 25 | // Init loads the list of apps that supports Apple Silicon from Does it ARM. 26 | func Init() error { 27 | resp, err := http.Get(sourceURL) 28 | if err != nil { 29 | return err 30 | } 31 | defer resp.Body.Close() 32 | 33 | body, err := ioutil.ReadAll(resp.Body) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | matches := pattern.FindAllStringSubmatch(string(body), -1) 39 | 40 | for _, match := range matches { 41 | name := match[1] 42 | info := AppInfo{ 43 | Website: match[2], 44 | } 45 | info.ArmSupport.Parse(match[3]) 46 | infoCache[name] = info 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // GetInfo returns the info of an app from remote sources, given the app name. 53 | func GetInfo(name string) (AppInfo, error) { 54 | if info, ok := infoCache[name]; ok { 55 | return info, nil 56 | } 57 | return AppInfo{}, ErrNotFound 58 | } 59 | -------------------------------------------------------------------------------- /internal/remotecheck/remote_test.go: -------------------------------------------------------------------------------- 1 | package remotecheck 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestInit(t *testing.T) { 10 | err := Init() 11 | assert.Nil(t, err) 12 | } 13 | 14 | func TestGetInfo(t *testing.T) { 15 | err := Init() 16 | assert.Nil(t, err) 17 | 18 | info, err := GetInfo("Go (golang)") 19 | assert.Nil(t, err) 20 | assert.Equal(t, SupportNative, info.ArmSupport) 21 | 22 | info, err = GetInfo("nonexist-app") 23 | assert.NotNil(t, err) 24 | assert.Equal(t, ErrNotFound, err) 25 | assert.Empty(t, info) 26 | } 27 | -------------------------------------------------------------------------------- /internal/remotecheck/support.go: -------------------------------------------------------------------------------- 1 | package remotecheck 2 | 3 | // Support represents the Arm support status of an app. 4 | type Support uint 5 | 6 | const ( 7 | // SupportUndefined is the zero value of Support type. 8 | SupportUndefined Support = iota // zero value 9 | // SupportNative means an app have native Apple Silicon support. 10 | SupportNative 11 | // SupportTransition means an app is supported vis Rosetta 2 or Virtual Environment. 12 | SupportTransition 13 | // SupportInDevelopment means an app does not support Apple Silicon yet but the support is in development. 14 | SupportInDevelopment 15 | // SupportNotYet means an app does not support Apple Silicon. 16 | SupportNotYet 17 | // SupportUnknown means it's not known if an app supports Apple Silicon. 18 | SupportUnknown 19 | ) 20 | 21 | // Parse parses support information from string. 22 | func (s *Support) Parse(str string) Support { 23 | switch str { 24 | case "✅": 25 | *s = SupportNative 26 | 27 | case "✳️": 28 | *s = SupportTransition 29 | 30 | case "⏹": 31 | *s = SupportInDevelopment 32 | 33 | case "🚫": 34 | *s = SupportNotYet 35 | 36 | case "🔶": 37 | *s = SupportUnknown 38 | 39 | } 40 | 41 | return *s 42 | } 43 | 44 | func (s Support) String() string { 45 | switch s { 46 | case SupportNative: 47 | return "Supported" 48 | 49 | case SupportTransition: 50 | return "Supported*" 51 | 52 | case SupportInDevelopment: 53 | return "Unsupported" 54 | 55 | case SupportNotYet: 56 | return "Unsupported" 57 | 58 | case SupportUnknown: 59 | return "Unknown" 60 | 61 | default: 62 | return "Unknown" 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/remotecheck/support_test.go: -------------------------------------------------------------------------------- 1 | package remotecheck 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSupport_Parse(t *testing.T) { 10 | tests := []struct { 11 | in string 12 | expected Support 13 | }{ 14 | {"✅", SupportNative}, 15 | {"✳️", SupportTransition}, 16 | {"⏹", SupportInDevelopment}, 17 | {"🚫", SupportNotYet}, 18 | {"🔶", SupportUnknown}, 19 | {"some other", SupportUndefined}, 20 | } 21 | 22 | for _, test := range tests { 23 | var support Support 24 | actual := support.Parse(test.in) 25 | 26 | assert.Equal(t, test.expected, support) 27 | assert.Equal(t, test.expected, actual) 28 | } 29 | } 30 | 31 | func TestSupport_String(t *testing.T) { 32 | tests := []struct { 33 | support Support 34 | expected string 35 | }{ 36 | {SupportNative, "Supported"}, 37 | {SupportTransition, "Supported*"}, 38 | {SupportInDevelopment, "Unsupported"}, 39 | {SupportNotYet, "Unsupported"}, 40 | {SupportUnknown, "Unknown"}, 41 | {SupportUndefined, "Unknown"}, 42 | } 43 | 44 | for _, test := range tests { 45 | actual := test.support.String() 46 | assert.Equal(t, test.expected, actual) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/harryzcy/ascheck/internal/macapp" 9 | "github.com/harryzcy/ascheck/internal/output" 10 | "github.com/harryzcy/ascheck/internal/remotecheck" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | // handleErr prints error and calls os.Exit(1) if err is not nil. 15 | func handleErr(err error) { 16 | if err != nil { 17 | fmt.Println(err) 18 | os.Exit(1) 19 | } 20 | } 21 | 22 | func main() { 23 | app := &cli.App{ 24 | Usage: "A cli app that check app's Apple Silicon support", 25 | Version: "0.2.0", 26 | HideHelpCommand: true, 27 | UsageText: "ascheck [global options]", 28 | Flags: []cli.Flag{ 29 | &cli.BoolFlag{ 30 | Name: "json", 31 | Usage: "output in json format", 32 | }, 33 | }, 34 | Action: func(c *cli.Context) error { 35 | err := remotecheck.Init() 36 | handleErr(err) 37 | 38 | apps, err := macapp.GetAllApplications(nil) 39 | handleErr(err) 40 | 41 | if c.Bool("json") { 42 | output.JSON(apps) 43 | } else { 44 | output.Table(apps) 45 | } 46 | return nil 47 | }, 48 | } 49 | 50 | err := app.Run(os.Args) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/data/bash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | -------------------------------------------------------------------------------- /test/data/env_bash.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | -------------------------------------------------------------------------------- /test/data/env_invalid.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env invalid 2 | -------------------------------------------------------------------------------- /test/data/example_fat.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | example 7 | CFBundleIdentifier 8 | com.example.io 9 | CFBundleName 10 | Test App 11 | CFBundleIconFile 12 | main.icns 13 | CFBundleShortVersionString 14 | $PKG_VERSION 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundlePackageType 18 | APPL 19 | IFMajorVersion 20 | 0 21 | IFMinorVersion 22 | 1 23 | NSSupportsAutomaticGraphicsSwitching 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/data/example_fat.app/Contents/MacOS/example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harryzcy/ascheck/32ebf24dcd6795f943e00882fe4050ea4128410f/test/data/example_fat.app/Contents/MacOS/example -------------------------------------------------------------------------------- /test/data/example_macho.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | example 7 | CFBundleIdentifier 8 | com.example.io 9 | CFBundleName 10 | Test App 11 | CFBundleIconFile 12 | main.icns 13 | CFBundleShortVersionString 14 | $PKG_VERSION 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundlePackageType 18 | APPL 19 | IFMajorVersion 20 | 0 21 | IFMinorVersion 22 | 1 23 | NSSupportsAutomaticGraphicsSwitching 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/data/example_macho.app/Contents/MacOS/example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harryzcy/ascheck/32ebf24dcd6795f943e00882fe4050ea4128410f/test/data/example_macho.app/Contents/MacOS/example -------------------------------------------------------------------------------- /test/data/invalid_interpreter.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | run.sh 7 | CFBundleIdentifier 8 | com.example.io 9 | CFBundleName 10 | Test App 11 | CFBundleIconFile 12 | main.icns 13 | CFBundleShortVersionString 14 | $PKG_VERSION 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundlePackageType 18 | APPL 19 | IFMajorVersion 20 | 0 21 | IFMinorVersion 22 | 1 23 | NSSupportsAutomaticGraphicsSwitching 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/data/invalid_interpreter.app/Contents/MacOS/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env invalid 2 | -------------------------------------------------------------------------------- /test/data/invalid_plist.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | invalid 2 | -------------------------------------------------------------------------------- /test/data/invalid_shebang.sh: -------------------------------------------------------------------------------- 1 | invalid 2 | -------------------------------------------------------------------------------- /test/data/sh_app.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | run.sh 7 | CFBundleIdentifier 8 | com.example.io 9 | CFBundleName 10 | Test App 11 | CFBundleIconFile 12 | main.icns 13 | CFBundleShortVersionString 14 | $PKG_VERSION 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundlePackageType 18 | APPL 19 | IFMajorVersion 20 | 0 21 | IFMinorVersion 22 | 1 23 | NSSupportsAutomaticGraphicsSwitching 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/data/sh_app.app/Contents/MacOS/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | -------------------------------------------------------------------------------- /test/data/unknown_type.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | example 7 | CFBundleIdentifier 8 | com.example.io 9 | CFBundleName 10 | Test App 11 | CFBundleIconFile 12 | main.icns 13 | CFBundleShortVersionString 14 | $PKG_VERSION 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundlePackageType 18 | APPL 19 | IFMajorVersion 20 | 0 21 | IFMinorVersion 22 | 1 23 | NSSupportsAutomaticGraphicsSwitching 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------