2017-03-30 02:29:06 +02:00
// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
2019-08-29 23:47:40 +02:00
"bytes"
2017-09-27 01:46:10 +02:00
"errors"
2017-11-01 21:33:02 +01:00
"flag"
2017-03-30 02:29:06 +02:00
"fmt"
"io/ioutil"
"os"
"os/exec"
2020-11-20 19:44:31 +01:00
"path"
2017-03-30 02:29:06 +02:00
"path/filepath"
"strings"
2019-03-29 21:54:39 +01:00
"time"
2019-08-29 23:47:40 +02:00
"android/soong/makedeps"
2017-03-30 02:29:06 +02:00
)
2017-11-01 21:33:02 +01:00
var (
sandboxesRoot string
2020-11-20 19:44:31 +01:00
rawCommand string
outputRoot string
2017-11-01 21:33:02 +01:00
keepOutDir bool
2020-11-20 19:44:31 +01:00
depfileOut string
inputHash string
2017-11-01 21:33:02 +01:00
)
func init ( ) {
flag . StringVar ( & sandboxesRoot , "sandbox-path" , "" ,
"root of temp directory to put the sandbox into" )
2020-11-20 19:44:31 +01:00
flag . StringVar ( & rawCommand , "c" , "" ,
"command to run" )
flag . StringVar ( & outputRoot , "output-root" , "" ,
"root of directory to copy outputs into" )
2017-11-01 21:33:02 +01:00
flag . BoolVar ( & keepOutDir , "keep-out-dir" , false ,
"whether to keep the sandbox directory when done" )
2020-11-20 19:44:31 +01:00
flag . StringVar ( & depfileOut , "depfile-out" , "" ,
"file path of the depfile to generate. This value will replace '__SBOX_DEPFILE__' in the command and will be treated as an output but won't be added to __SBOX_OUT_FILES__" )
flag . StringVar ( & inputHash , "input-hash" , "" ,
"This option is ignored. Typical usage is to supply a hash of the list of input names so that the module will be rebuilt if the list (and thus the hash) changes." )
2017-11-01 21:33:02 +01:00
}
func usageViolation ( violation string ) {
if violation != "" {
fmt . Fprintf ( os . Stderr , "Usage error: %s.\n\n" , violation )
}
fmt . Fprintf ( os . Stderr ,
2020-11-20 19:44:31 +01:00
"Usage: sbox -c <commandToRun> --sandbox-path <sandboxPath> --output-root <outputRoot> [--depfile-out depFile] [--input-hash hash] <outputFile> [<outputFile>...]\n" +
"\n" +
"Deletes <outputRoot>," +
"runs <commandToRun>," +
"and moves each <outputFile> out of <sandboxPath> and into <outputRoot>\n" )
2017-11-01 21:33:02 +01:00
flag . PrintDefaults ( )
os . Exit ( 1 )
}
2017-03-30 02:29:06 +02:00
func main ( ) {
2017-11-01 21:33:02 +01:00
flag . Usage = func ( ) {
usageViolation ( "" )
}
flag . Parse ( )
2017-03-30 02:29:06 +02:00
error := run ( )
if error != nil {
fmt . Fprintln ( os . Stderr , error )
os . Exit ( 1 )
}
}
2017-09-27 01:46:10 +02:00
func findAllFilesUnder ( root string ) ( paths [ ] string ) {
paths = [ ] string { }
filepath . Walk ( root , func ( path string , info os . FileInfo , err error ) error {
if ! info . IsDir ( ) {
relPath , err := filepath . Rel ( root , path )
if err != nil {
// couldn't find relative path from ancestor?
panic ( err )
}
paths = append ( paths , relPath )
}
return nil
} )
return paths
}
2017-03-30 02:29:06 +02:00
func run ( ) error {
2020-11-20 19:44:31 +01:00
if rawCommand == "" {
usageViolation ( "-c <commandToRun> is required and must be non-empty" )
2017-03-30 02:29:06 +02:00
}
2017-10-27 23:59:27 +02:00
if sandboxesRoot == "" {
2017-03-30 02:29:06 +02:00
// In practice, the value of sandboxesRoot will mostly likely be at a fixed location relative to OUT_DIR,
// and the sbox executable will most likely be at a fixed location relative to OUT_DIR too, so
// the value of sandboxesRoot will most likely be at a fixed location relative to the sbox executable
// However, Soong also needs to be able to separately remove the sandbox directory on startup (if it has anything left in it)
// and by passing it as a parameter we don't need to duplicate its value
2017-11-01 21:33:02 +01:00
usageViolation ( "--sandbox-path <sandboxPath> is required and must be non-empty" )
2017-03-30 02:29:06 +02:00
}
2020-11-20 19:44:31 +01:00
if len ( outputRoot ) == 0 {
usageViolation ( "--output-root <outputRoot> is required and must be non-empty" )
}
2017-10-27 23:59:27 +02:00
2020-11-20 19:44:31 +01:00
// the contents of the __SBOX_OUT_FILES__ variable
outputsVarEntries := flag . Args ( )
if len ( outputsVarEntries ) == 0 {
usageViolation ( "at least one output file must be given" )
2017-11-06 22:33:14 +01:00
}
2020-11-12 17:29:30 +01:00
2020-11-20 19:44:31 +01:00
// all outputs
var allOutputs [ ] string
// setup directories
err := os . MkdirAll ( sandboxesRoot , 0777 )
2017-11-06 22:33:14 +01:00
if err != nil {
2020-11-20 19:44:31 +01:00
return err
2017-11-06 22:33:14 +01:00
}
2020-11-20 19:44:31 +01:00
err = os . RemoveAll ( outputRoot )
2017-11-06 22:33:14 +01:00
if err != nil {
2020-11-20 19:44:31 +01:00
return err
}
err = os . MkdirAll ( outputRoot , 0777 )
if err != nil {
return err
2017-11-06 22:33:14 +01:00
}
2017-10-27 23:59:27 +02:00
2020-11-20 19:44:31 +01:00
tempDir , err := ioutil . TempDir ( sandboxesRoot , "sbox" )
2017-03-30 02:29:06 +02:00
2020-11-20 19:44:31 +01:00
for i , filePath := range outputsVarEntries {
if ! strings . HasPrefix ( filePath , "__SBOX_OUT_DIR__/" ) {
return fmt . Errorf ( "output files must start with `__SBOX_OUT_DIR__/`" )
2017-06-13 00:00:12 +02:00
}
2020-11-20 19:44:31 +01:00
outputsVarEntries [ i ] = strings . TrimPrefix ( filePath , "__SBOX_OUT_DIR__/" )
2017-06-13 00:00:12 +02:00
}
2020-11-20 19:44:31 +01:00
allOutputs = append ( [ ] string ( nil ) , outputsVarEntries ... )
2017-10-27 23:59:27 +02:00
2020-11-20 19:44:31 +01:00
if depfileOut != "" {
sandboxedDepfile , err := filepath . Rel ( outputRoot , depfileOut )
2017-10-27 23:59:27 +02:00
if err != nil {
2020-11-20 19:44:31 +01:00
return err
2017-10-27 23:59:27 +02:00
}
2020-11-20 19:44:31 +01:00
allOutputs = append ( allOutputs , sandboxedDepfile )
rawCommand = strings . Replace ( rawCommand , "__SBOX_DEPFILE__" , filepath . Join ( tempDir , sandboxedDepfile ) , - 1 )
2017-10-27 23:59:27 +02:00
}
2017-03-30 02:29:06 +02:00
if err != nil {
2020-11-20 19:44:31 +01:00
return fmt . Errorf ( "Failed to create temp dir: %s" , err )
2017-03-30 02:29:06 +02:00
}
2020-11-20 19:44:31 +01:00
// In the common case, the following line of code is what removes the sandbox
// If a fatal error occurs (such as if our Go process is killed unexpectedly),
// then at the beginning of the next build, Soong will retry the cleanup
defer func ( ) {
// in some cases we decline to remove the temp dir, to facilitate debugging
if ! keepOutDir {
os . RemoveAll ( tempDir )
}
} ( )
2020-11-12 17:29:30 +01:00
2020-11-20 19:44:31 +01:00
if strings . Contains ( rawCommand , "__SBOX_OUT_DIR__" ) {
rawCommand = strings . Replace ( rawCommand , "__SBOX_OUT_DIR__" , tempDir , - 1 )
2020-11-12 17:29:30 +01:00
}
2020-11-20 19:44:31 +01:00
if strings . Contains ( rawCommand , "__SBOX_OUT_FILES__" ) {
// expands into a space-separated list of output files to be generated into the sandbox directory
tempOutPaths := [ ] string { }
for _ , outputPath := range outputsVarEntries {
tempOutPath := path . Join ( tempDir , outputPath )
tempOutPaths = append ( tempOutPaths , tempOutPath )
}
pathsText := strings . Join ( tempOutPaths , " " )
rawCommand = strings . Replace ( rawCommand , "__SBOX_OUT_FILES__" , pathsText , - 1 )
2020-11-12 17:29:30 +01:00
}
2020-11-20 19:44:31 +01:00
for _ , filePath := range allOutputs {
dir := path . Join ( tempDir , filepath . Dir ( filePath ) )
err = os . MkdirAll ( dir , 0777 )
if err != nil {
return err
}
2017-03-30 02:29:06 +02:00
}
2017-06-13 00:00:12 +02:00
commandDescription := rawCommand
2017-03-30 02:29:06 +02:00
cmd := exec . Command ( "bash" , "-c" , rawCommand )
cmd . Stdin = os . Stdin
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
err = cmd . Run ( )
2017-06-13 00:00:12 +02:00
2017-03-30 02:29:06 +02:00
if exit , ok := err . ( * exec . ExitError ) ; ok && ! exit . Success ( ) {
2020-11-20 19:44:31 +01:00
return fmt . Errorf ( "sbox command (%s) failed with err %#v\n" , commandDescription , err . Error ( ) )
2017-03-30 02:29:06 +02:00
} else if err != nil {
2020-11-20 19:44:31 +01:00
return err
2017-03-30 02:29:06 +02:00
}
2020-11-20 19:44:31 +01:00
// validate that all files are created properly
var missingOutputErrors [ ] string
for _ , filePath := range allOutputs {
tempPath := filepath . Join ( tempDir , filePath )
fileInfo , err := os . Stat ( tempPath )
if err != nil {
missingOutputErrors = append ( missingOutputErrors , fmt . Sprintf ( "%s: does not exist" , filePath ) )
continue
}
if fileInfo . IsDir ( ) {
missingOutputErrors = append ( missingOutputErrors , fmt . Sprintf ( "%s: not a file" , filePath ) )
}
}
2017-09-27 01:46:10 +02:00
if len ( missingOutputErrors ) > 0 {
// find all created files for making a more informative error message
createdFiles := findAllFilesUnder ( tempDir )
// build error message
errorMessage := "mismatch between declared and actual outputs\n"
errorMessage += "in sbox command(" + commandDescription + ")\n\n"
errorMessage += "in sandbox " + tempDir + ",\n"
errorMessage += fmt . Sprintf ( "failed to create %v files:\n" , len ( missingOutputErrors ) )
for _ , missingOutputError := range missingOutputErrors {
2020-11-20 19:44:31 +01:00
errorMessage += " " + missingOutputError + "\n"
2017-09-27 01:46:10 +02:00
}
if len ( createdFiles ) < 1 {
errorMessage += "created 0 files."
} else {
errorMessage += fmt . Sprintf ( "did create %v files:\n" , len ( createdFiles ) )
creationMessages := createdFiles
maxNumCreationLines := 10
if len ( creationMessages ) > maxNumCreationLines {
creationMessages = creationMessages [ : maxNumCreationLines ]
creationMessages = append ( creationMessages , fmt . Sprintf ( "...%v more" , len ( createdFiles ) - maxNumCreationLines ) )
}
for _ , creationMessage := range creationMessages {
errorMessage += " " + creationMessage + "\n"
}
}
2020-11-20 19:44:31 +01:00
// Keep the temporary output directory around in case a user wants to inspect it for debugging purposes.
// Soong will delete it later anyway.
keepOutDir = true
return errors . New ( errorMessage )
2017-06-07 22:22:22 +02:00
}
// the created files match the declared files; now move them
2020-11-20 19:44:31 +01:00
for _ , filePath := range allOutputs {
tempPath := filepath . Join ( tempDir , filePath )
destPath := filePath
if len ( outputRoot ) != 0 {
destPath = filepath . Join ( outputRoot , filePath )
2019-03-29 21:54:39 +01:00
}
2020-11-20 19:44:31 +01:00
err := os . MkdirAll ( filepath . Dir ( destPath ) , 0777 )
2017-03-30 02:29:06 +02:00
if err != nil {
return err
}
2017-06-07 22:22:22 +02:00
2020-11-20 19:44:31 +01:00
// Update the timestamp of the output file in case the tool wrote an old timestamp (for example, tar can extract
// files with old timestamps).
now := time . Now ( )
err = os . Chtimes ( tempPath , now , now )
2019-08-29 23:47:40 +02:00
if err != nil {
return err
}
2020-11-20 19:44:31 +01:00
err = os . Rename ( tempPath , destPath )
2019-08-29 23:47:40 +02:00
if err != nil {
return err
}
2020-11-20 19:44:31 +01:00
}
2019-08-29 23:47:40 +02:00
2020-11-20 19:44:31 +01:00
// Rewrite the depfile so that it doesn't include the (randomized) sandbox directory
if depfileOut != "" {
in , err := ioutil . ReadFile ( depfileOut )
2020-11-12 17:29:30 +01:00
if err != nil {
return err
}
2020-11-20 19:44:31 +01:00
deps , err := makedeps . Parse ( depfileOut , bytes . NewBuffer ( in ) )
2020-11-12 17:29:30 +01:00
if err != nil {
return err
}
2019-08-29 23:47:40 +02:00
2020-11-20 19:44:31 +01:00
deps . Output = "outputfile"
err = ioutil . WriteFile ( depfileOut , deps . Print ( ) , 0666 )
2019-08-29 23:47:40 +02:00
if err != nil {
return err
}
2020-11-12 17:29:30 +01:00
}
2020-11-20 19:44:31 +01:00
// TODO(jeffrygaston) if a process creates more output files than it declares, should there be a warning?
return nil
2017-03-30 02:29:06 +02:00
}