github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/agent/bundle.go (about) 1 package agent 2 3 import ( 4 "archive/tar" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "runtime" 11 12 "github.com/klauspost/compress/gzip" 13 14 "github.com/mutagen-io/mutagen/pkg/filesystem" 15 "github.com/mutagen-io/mutagen/pkg/mutagen" 16 "github.com/mutagen-io/mutagen/pkg/platform" 17 ) 18 19 const ( 20 // BundleName is the base name of the agent bundle. 21 BundleName = "mutagen-agents.tar.gz" 22 ) 23 24 // BundleLocation encodes an expected location for the agent bundle. 25 type BundleLocation uint8 26 27 const ( 28 // BundleLocationDefault indicates that the ExecutableForPlatform function 29 // should expect to find the agent bundle in the same directory as the 30 // current executable or (if the current executable resides in the "bin" 31 // directory of a Filesystem Hierarchy Standard layout) in the libexec 32 // directory. 33 BundleLocationDefault BundleLocation = iota 34 // BundleLocationBuildDirectory indicates that the ExecutableForPlatform 35 // function should expect to find the agent bundle in the Mutagen build 36 // directory. This mode is only used during integration testing. It is 37 // required because test executables will be built in temporary directories. 38 BundleLocationBuildDirectory 39 ) 40 41 // ExpectedBundleLocation specifies the expected agent bundle location. It is 42 // set by the time that init functions have completed. After that, it should 43 // only be set at the process entry point, before any code calls into the agent 44 // package. 45 var ExpectedBundleLocation BundleLocation 46 47 // ExecutableForPlatform attempts to locate the agent bundle and extract an 48 // agent executable for the specified target platform. If no output path is 49 // specified, then the extracted file will be in a temporary location accessible 50 // to only the user, will have an appropriate extension for the target platform, 51 // and will have the executability bit set if it makes sense. The path to the 52 // extracted file will be returned, and the caller is responsible for cleaning 53 // up the file if this function returns a nil error. 54 func ExecutableForPlatform(goos, goarch, outputPath string) (string, error) { 55 // Compute the path to the location in which we expect to find the agent 56 // bundle. 57 var bundleSearchPaths []string 58 if ExpectedBundleLocation == BundleLocationDefault { 59 // Add the executable directory as a search path. 60 if executablePath, err := os.Executable(); err != nil { 61 return "", fmt.Errorf("unable to determine executable path: %w", err) 62 } else { 63 bundleSearchPaths = append(bundleSearchPaths, filepath.Dir(executablePath)) 64 } 65 66 // If the executable is in what appears to be a Filesystem Hierarchy 67 // Standard layout, then add the libexec directory as a search path. 68 if libexecPath, err := filesystem.LibexecPath(); err == nil { 69 bundleSearchPaths = append(bundleSearchPaths, libexecPath) 70 } 71 } else if ExpectedBundleLocation == BundleLocationBuildDirectory { 72 if sourceTreePath, err := mutagen.SourceTreePath(); err != nil { 73 return "", fmt.Errorf("unable to determine Mutagen source tree path: %w", err) 74 } else { 75 bundleSearchPaths = append(bundleSearchPaths, filepath.Join(sourceTreePath, mutagen.BuildDirectoryName)) 76 } 77 } else { 78 panic("invalid bundle location specification") 79 } 80 81 // Loop until we find a bundle file. If we fail to locate a bundle, then 82 // abort. If we succeed, then defer its closure. 83 var bundle *os.File 84 for _, path := range bundleSearchPaths { 85 bundlePath := filepath.Join(path, BundleName) 86 if file, err := os.Open(bundlePath); err != nil { 87 if os.IsNotExist(err) { 88 continue 89 } 90 return "", fmt.Errorf("unable to open agent bundle (%s): %w", bundlePath, err) 91 } else if metadata, err := file.Stat(); err != nil { 92 file.Close() 93 return "", fmt.Errorf("unable to access agent bundle (%s) file metadata: %w", bundlePath, err) 94 } else if metadata.Mode()&os.ModeType != 0 { 95 file.Close() 96 return "", fmt.Errorf("agent bundle (%s) is not a file", bundlePath) 97 } else { 98 bundle = file 99 defer bundle.Close() 100 } 101 } 102 if bundle == nil { 103 return "", fmt.Errorf("unable to locate agent bundle (search paths: %v)", bundleSearchPaths) 104 } 105 106 // Create a decompressor and defer its closure. 107 bundleDecompressor, err := gzip.NewReader(bundle) 108 if err != nil { 109 return "", fmt.Errorf("unable to decompress agent bundle: %w", err) 110 } 111 defer bundleDecompressor.Close() 112 113 // Create an archive reader. 114 bundleArchive := tar.NewReader(bundleDecompressor) 115 116 // Scan until we find a matching header. 117 var header *tar.Header 118 for { 119 if h, err := bundleArchive.Next(); err != nil { 120 if err == io.EOF { 121 break 122 } 123 return "", fmt.Errorf("unable to read archive header: %w", err) 124 } else if h.Name == fmt.Sprintf("%s_%s", goos, goarch) { 125 header = h 126 break 127 } 128 } 129 130 // Check if we have a valid header. If not, there was no match. 131 if header == nil { 132 return "", errors.New("unsupported platform") 133 } 134 135 // If an output path has been specified, then open the path for writing, 136 // otherwise create a temporary file. 137 var file *os.File 138 if outputPath != "" { 139 file, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 140 } else { 141 file, err = os.CreateTemp("", platform.ExecutableName(BaseName+".*", goos)) 142 } 143 if err != nil { 144 return "", fmt.Errorf("unable to create output file: %w", err) 145 } 146 147 // Copy data into the file. 148 if _, err := io.CopyN(file, bundleArchive, header.Size); err != nil { 149 file.Close() 150 os.Remove(file.Name()) 151 return "", fmt.Errorf("unable to copy agent data: %w", err) 152 } 153 154 // If we're not on Windows and our target system is not Windows, mark the 155 // file as executable. This will save us an additional "chmod +x" command 156 // during agent installation. Note that the mechanism we use here 157 // (os.File.Chmod) does not work on Windows (only the path-based os.Chmod is 158 // supported there), but this is fine because this code wouldn't make sense 159 // to use on Windows in any scenario (where executability bits don't exist). 160 if runtime.GOOS != "windows" && goos != "windows" { 161 if err := file.Chmod(0700); err != nil { 162 file.Close() 163 os.Remove(file.Name()) 164 return "", fmt.Errorf("unable to make agent executable: %w", err) 165 } 166 } 167 168 // Close the file. 169 if err := file.Close(); err != nil { 170 os.Remove(file.Name()) 171 return "", fmt.Errorf("unable to close temporary file: %w", err) 172 } 173 174 // Success. 175 return file.Name(), nil 176 }