github.com/yourbase/yb@v0.7.1/cmd/yb/keychain.go (about) 1 // Copyright 2021 YourBase Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // SPDX-License-Identifier: Apache-2.0 16 17 package main 18 19 import ( 20 "context" 21 "fmt" 22 "path/filepath" 23 "strings" 24 25 "github.com/yourbase/yb/internal/biome" 26 "zombiezen.com/go/log" 27 ) 28 29 // ensureKeychain ensures that a default keychain is present in the biome. 30 // If the biome is not a macOS environment, then ensureKeychain does nothing. 31 func ensureKeychain(ctx context.Context, bio biome.Biome) error { 32 if bio.Describe().OS != biome.MacOS { 33 return nil 34 } 35 36 // Check whether a default keychain already exists. 37 stdout := new(strings.Builder) 38 stderr := new(strings.Builder) 39 err := bio.Run(ctx, &biome.Invocation{ 40 Argv: []string{"security", "default-keychain", "-d", "user"}, 41 Stdout: stdout, 42 Stderr: stderr, 43 }) 44 if err == nil && len(parseKeychainOutput(stdout.String())) > 0 { 45 // Keychain already exists. 46 return nil 47 } 48 if err != nil { 49 if stderr.Len() > 0 { 50 log.Debugf(ctx, "No default keychain; will create. Error:\n%s%v", stderr, err) 51 } else { 52 log.Debugf(ctx, "No default keychain; will create. Error: %v", err) 53 } 54 } 55 56 // From experimentation, `security list-keychains -s` will silently fail 57 // unless ~/Library/Preferences exists. 58 if err := biome.MkdirAll(ctx, bio, bio.JoinPath(bio.Dirs().Home, "Library", "Preferences")); err != nil { 59 return fmt.Errorf("ensure build environment keychain: %w", err) 60 } 61 62 // List the existing user keychains. There likely won't be any, but for 63 // robustness, we preserve them in the search path. 64 stdout.Reset() 65 stderr.Reset() 66 err = bio.Run(ctx, &biome.Invocation{ 67 Argv: []string{"security", "list-keychains", "-d", "user"}, 68 Stdout: stdout, 69 Stderr: stderr, 70 }) 71 if err != nil { 72 if stderr.Len() > 0 { 73 // stderr will almost certainly end in '\n'. 74 return fmt.Errorf("ensure build environment keychain: %s%w", stderr, err) 75 } 76 return fmt.Errorf("ensure build environment keychain: %w", err) 77 } 78 keychainList := parseKeychainOutput(stdout.String()) 79 80 // Create a passwordless keychain. 81 const keychainName = "login.keychain" 82 if err := runCommand(ctx, bio, "security", "create-keychain", "-p", "", keychainName); err != nil { 83 return fmt.Errorf("ensure build environment keychain: %w", err) 84 } 85 86 // The keychain must be added to the search path. 87 // See https://stackoverflow.com/questions/20391911/os-x-keychain-not-visible-to-keychain-access-app-in-mavericks 88 // 89 // We prepend it to the search path so that Fastlane picks it up: 90 // https://github.com/fastlane/fastlane/blob/832e3e4a19d9cff5d5a14a61e9614b5659327427/fastlane_core/lib/fastlane_core/cert_checker.rb#L133-L134 91 searchPathArgs := []string{"security", "list-keychains", "-d", "user", "-s", keychainName} 92 for _, k := range keychainList { 93 searchPathArgs = append(searchPathArgs, filepath.Base(k)) 94 } 95 if err := runCommand(ctx, bio, searchPathArgs...); err != nil { 96 return fmt.Errorf("ensure build environment keychain: %w", err) 97 } 98 99 // Set the new keychain as the default. 100 if err := runCommand(ctx, bio, "security", "default-keychain", "-s", keychainName); err != nil { 101 return fmt.Errorf("ensure build environment keychain: %w", err) 102 } 103 return nil 104 } 105 106 func parseKeychainOutput(out string) []string { 107 lines := strings.Split(out, "\n") 108 if lines[len(lines)-1] == "" { 109 lines = lines[:len(lines)-1] 110 } 111 paths := make([]string, 0, len(lines)) 112 for _, line := range lines { 113 line = strings.TrimSpace(line) 114 if !strings.HasPrefix(line, `"`) || !strings.HasSuffix(line, `"`) { 115 continue 116 } 117 paths = append(paths, line[1:len(line)-1]) 118 } 119 return paths 120 }