diff --git a/Gopkg.lock b/Gopkg.lock index cad27c7..f0fbb56 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,12 +3,13 @@ [[projects]] branch = "master" - digest = "1:afe05c8adf1e427aba44ff9a3fba6d0eb766244cfc367dd4332583bf925533b1" + digest = "1:43b082cf688ce011c5d2b0b7e81d610911e02209fb6ee2e3d206280f560499f1" name = "github.com/bitrise-io/go-utils" packages = [ "colorstring", "command", "errorutil", + "fileutil", "log", "parseutil", "pathutil", @@ -25,13 +26,24 @@ pruneopts = "UT" revision = "d4d9e08cc4347e8784bb18419fcdceb932e17019" +[[projects]] + branch = "master" + digest = "1:3d8c80c59d85c2ecf2b037c512d29016343956c100b6338bd923c1fd22ab4bb3" + name = "github.com/bitrise-tools/xcode-project" + packages = ["serialized"] + pruneopts = "UT" + revision = "3095d887a8e540536646a9483ac5cafb94768ffc" + [solve-meta] analyzer-name = "dep" analyzer-version = 1 input-imports = [ "github.com/bitrise-io/go-utils/command", + "github.com/bitrise-io/go-utils/fileutil", "github.com/bitrise-io/go-utils/log", + "github.com/bitrise-io/go-utils/pathutil", "github.com/bitrise-tools/go-steputils/stepconf", + "github.com/bitrise-tools/xcode-project/serialized", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/bitrise.yml b/bitrise.yml index 72de4dd..6633f95 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -5,15 +5,15 @@ app: envs: - BITRISE_STEP_GIT_CLONE_URL: https://github.com/bitrise-steplib/steps-expo-detach.git + - ORIGIN_SOURCE_DIR: $BITRISE_SOURCE_DIR + - SAMPLE_APP_URL: https://github.com/bitrise-samples/react-native-expo.git + # Define it in .bitrise.secrets.yml - USER_NAME: $USER_NAME - PASSWORD: $PASSWORD workflows: test: - envs: - - ORIGIN_SOURCE_DIR: $BITRISE_SOURCE_DIR - - SAMPLE_APP_URL: https://github.com/bitrise-samples/react-native-expo.git before_run: - audit-this-step steps: @@ -45,6 +45,7 @@ workflows: inputs: - project_path: $BITRISE_SOURCE_DIR - expo_cli_verson: "latest" + - override_react_native_version: 0.55.4 plain_2_0_0: before_run: @@ -130,6 +131,21 @@ workflows: - path: ${ORIGIN_SOURCE_DIR}/_tmp - is_create_path: true + dep-update: + title: Dep update + description: | + Used for updating bitrise dependencies with dep + steps: + - script: + title: Dependency update + inputs: + - content: |- + #!/bin/bash + set -ex + go get -u -v github.com/golang/dep/cmd/dep + dep ensure -v + dep ensure -v -update + # ---------------------------------------------------------------- # --- workflows to Share this step into a Step Library audit-this-step: diff --git a/main.go b/main.go index b890f88..5531549 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,29 @@ package main import ( + "encoding/json" "fmt" "os" "path/filepath" "strings" "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/errorutil" + "github.com/bitrise-io/go-utils/fileutil" "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/go-utils/pathutil" "github.com/bitrise-tools/go-steputils/stepconf" + "github.com/bitrise-tools/xcode-project/serialized" ) // Config ... type Config struct { - ProjectPath string `env:"project_path,dir"` - ExpoCLIVersion string `env:"expo_cli_verson,required"` - UserName string `env:"user_name"` - Password stepconf.Secret `env:"password"` + Workdir string `env:"project_path,dir"` + ExpoCLIVersion string `env:"expo_cli_verson,required"` + UserName string `env:"user_name"` + Password stepconf.Secret `env:"password"` + RunPublish string `env:"run_publish"` + OverrideReactNativeVersion string `env:"override_react_native_version"` } // EjectMethod if the project is using Expo SDK and you choose the "plain" --eject-method those imports will stop working. @@ -28,14 +35,11 @@ const ( ExpoKit EjectMethod = "expoKit" ) -func (m EjectMethod) String() string { - return string(m) -} - // Expo CLI type Expo struct { Version string Method EjectMethod + Workdir string } // installExpoCLI runs the install npm command to install the expo-cli @@ -51,7 +55,7 @@ func (e Expo) installExpoCLI() error { cmd.SetStdout(os.Stdout) cmd.SetStderr(os.Stderr) - log.Printf("$ " + cmd.PrintableCommandArgs()) + log.Donef("$ " + cmd.PrintableCommandArgs()) return cmd.Run() } @@ -82,20 +86,56 @@ func (e Expo) logout() error { // Eject command creates Xcode and Android Studio projects for your app. func (e Expo) eject() error { - args := []string{"eject", "--non-interactive", "--eject-method", e.Method.String()} + args := []string{"eject", "--non-interactive", "--eject-method", string(e.Method)} cmd := command.New("expo", args...) cmd.SetStdout(os.Stdout) cmd.SetStderr(os.Stderr) + if e.Workdir != "" { + cmd.SetDir(e.Workdir) + } - log.Printf("$ " + cmd.PrintableCommandArgs()) + log.Donef("$ " + cmd.PrintableCommandArgs()) return cmd.Run() } -func failf(format string, v ...interface{}) { - log.Errorf(format, v...) - log.Warnf("For more details you can enable the debug logs by turning on the verbose step input.") - os.Exit(1) +func (e Expo) publish() error { + args := []string{"publish", "--non-interactive"} + + cmd := command.New("expo", args...) + cmd.SetStdout(os.Stdout) + cmd.SetStderr(os.Stderr) + if e.Workdir != "" { + cmd.SetDir(e.Workdir) + } + + log.Donef("$ " + cmd.PrintableCommandArgs()) + return cmd.Run() +} + +func parsePackageJSON(pth string) (serialized.Object, error) { + b, err := fileutil.ReadBytesFromFile(pth) + if err != nil { + return nil, fmt.Errorf("Failed to read package.json file: %s", err) + } + + var packages serialized.Object + if err := json.Unmarshal(b, &packages); err != nil { + return nil, fmt.Errorf("Failed to parse package.json file: %s", err) + } + return packages, nil +} + +func savePackageJSON(packages serialized.Object, pth string) error { + b, err := json.MarshalIndent(packages, "", " ") + if err != nil { + return fmt.Errorf("Failed to serialize modified package.json file: %s", err) + } + + if err := fileutil.WriteBytesToFile(pth, b); err != nil { + return fmt.Errorf("Failed to write modified package.json file: %s", err) + } + return nil } func validateUserNameAndpassword(userName string, password stepconf.Secret) error { @@ -109,6 +149,11 @@ func validateUserNameAndpassword(userName string, password stepconf.Secret) erro return nil } +func failf(format string, v ...interface{}) { + log.Errorf(format, v...) + os.Exit(1) +} + func main() { var cfg Config if err := stepconf.Parse(&cfg); err != nil { @@ -139,6 +184,7 @@ func main() { e := Expo{ Version: cfg.ExpoCLIVersion, Method: ejectMethod, + Workdir: cfg.Workdir, } // @@ -190,11 +236,65 @@ func main() { log.Infof("Eject project") { if err := e.eject(); err != nil { - failf("Failed to eject project (%s), error: %s", filepath.Base(cfg.ProjectPath), err) + failf("Failed to eject project: %s", err) } } fmt.Println() log.Donef("Successfully ejected your project") + + if cfg.RunPublish == "yes" { + fmt.Println() + log.Infof("Running expo publish") + + if err := e.publish(); err != nil { + failf("Failed to publish project: %s", err) + } + } + + if cfg.OverrideReactNativeVersion != "" { + // + // Force certain version of React Native in package.json file + fmt.Println() + log.Infof("Set react-native dependency version: %s", cfg.OverrideReactNativeVersion) + + packageJSONPth := filepath.Join(cfg.Workdir, "package.json") + packages, err := parsePackageJSON(packageJSONPth) + if err != nil { + failf(err.Error()) + } + + deps, err := packages.Object("dependencies") + if err != nil { + failf("Failed to parse dependencies from package.json file: %s", err) + } + + deps["react-native"] = cfg.OverrideReactNativeVersion + packages["dependencies"] = deps + + if err := savePackageJSON(packages, packageJSONPth); err != nil { + failf(err.Error()) + } + + // + // Install new node dependencies + log.Printf("install new node dependencies") + + nodeDepManager := "npm" + if exist, err := pathutil.IsPathExists(filepath.Join(cfg.Workdir, "yarn.lock")); err != nil { + log.Warnf("Failed to check if yarn.lock file exists in the workdir: %s", err) + } else if exist { + nodeDepManager = "yarn" + } + + cmd := command.New(nodeDepManager, "install") + out, err := cmd.RunAndReturnTrimmedCombinedOutput() + if err != nil { + if errorutil.IsExitStatusError(err) { + failf("%s failed: %s", cmd.PrintableCommandArgs(), out) + } + failf("%s failed: %s", cmd.PrintableCommandArgs(), err) + } + } } diff --git a/step.yml b/step.yml index b4c80e0..8e191de 100644 --- a/step.yml +++ b/step.yml @@ -22,25 +22,21 @@ toolkit: inputs: - project_path: $BITRISE_SOURCE_DIR opts: - title: Project path - summary: Project path + title: Working directory + summary: The root directory of the React Native project description: |- - The path of your project directory - is_required: true + The root directory of the React Native project (the directory of the project package.js file). - expo_cli_verson: "latest" opts: title: Expo CLI version summary: Specify the Expo CLI version to install. description: |- - Specify the Expo CLI version to install. + Specify the Expo CLI version to install. The Expo CLI ejects your project and creates Xcode and Android Studio projects for your app. - [https://docs.expo.io/versions/latest/introduction/installation#local-development-tool-expo-cli](https://docs.expo.io/versions/latest/introduction/installation#local-development-tool-expo-cli) - A couple of examples: - * "2.0.0" * latest @@ -52,12 +48,48 @@ inputs: description: |- Your account's username for `https://expo.io/` . + In case of React Native project __using Expo Kit__ library (any .js file imports expo), + the `user_name` and `password` inputs are __required__. + + If you provide these inputs the step will run: `expo eject --eject-method expoKit`, + otherwise: `expo eject --eject-method plain`. - **NOTE** You need to use your username and not your e-mail address. + **NOTE:** You need to use your username and not your e-mail address. - password: "" opts: title: Password for your Expo account summary: Password for your Expo account. description: |- Your password for `https://expo.io/` . - is_sensitive: true \ No newline at end of file + + In case of React Native project __using Expo Kit__ library (any .js file imports expo), + the `user_name` and `password` inputs are __required__. + + If you provide these inputs the step will run: `expo eject --eject-method expoKit`, + otherwise: `expo eject --eject-method plain`. + is_sensitive: true + - run_publish: "no" + opts: + title: Run expo publish after eject? + summary: Should the step run `expo publish` after eject? + description: |- + Should the step run `expo publish` after eject? + + In case of React Native project using Expo Kit library (any .js file imports expo), + `expo publis` command generates the: + + - ./android/app/src/main/assets/shell-app-manifest.json + - ./android/app/src/main/assets/shell-app.bundle + - ./ios/bitriseexpokit/Supporting/shell-app-manifest.json + - ./ios/bitriseexpokit/Supporting/shell-app.bundle + + files, which are required for the native builds. + value_options: + - "yes" + - "no" + - override_react_native_version: + opts: + title: React Native version to set in package.json + summary: React Native version to set in package.json after the eject process. + description: |- + React Native version to set in package.json after the eject process. diff --git a/vendor/github.com/bitrise-io/go-utils/fileutil/fileutil.go b/vendor/github.com/bitrise-io/go-utils/fileutil/fileutil.go new file mode 100644 index 0000000..c2c2fb9 --- /dev/null +++ b/vendor/github.com/bitrise-io/go-utils/fileutil/fileutil.go @@ -0,0 +1,138 @@ +package fileutil + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "os" + + "github.com/bitrise-io/go-utils/pathutil" +) + +// WriteStringToFile ... +func WriteStringToFile(pth string, fileCont string) error { + return WriteBytesToFile(pth, []byte(fileCont)) +} + +// WriteStringToFileWithPermission ... +func WriteStringToFileWithPermission(pth string, fileCont string, perm os.FileMode) error { + return WriteBytesToFileWithPermission(pth, []byte(fileCont), perm) +} + +// WriteBytesToFileWithPermission ... +func WriteBytesToFileWithPermission(pth string, fileCont []byte, perm os.FileMode) error { + if pth == "" { + return errors.New("No path provided") + } + + var file *os.File + var err error + if perm == 0 { + file, err = os.Create(pth) + } else { + // same as os.Create, but with a specified permission + // the flags are copy-pasted from the official + // os.Create func: https://golang.org/src/os/file.go?s=7327:7366#L244 + file, err = os.OpenFile(pth, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm) + } + if err != nil { + return err + } + defer func() { + if err := file.Close(); err != nil { + log.Println(" [!] Failed to close file:", err) + } + }() + + if _, err := file.Write(fileCont); err != nil { + return err + } + + return nil +} + +// WriteBytesToFile ... +func WriteBytesToFile(pth string, fileCont []byte) error { + return WriteBytesToFileWithPermission(pth, fileCont, 0) +} + +// AppendStringToFile ... +func AppendStringToFile(pth string, fileCont string) error { + return AppendBytesToFile(pth, []byte(fileCont)) +} + +// AppendBytesToFile ... +func AppendBytesToFile(pth string, fileCont []byte) error { + if pth == "" { + return errors.New("No path provided") + } + + var file *os.File + filePerm, err := GetFilePermissions(pth) + if err != nil { + // create the file + file, err = os.Create(pth) + } else { + // open for append + file, err = os.OpenFile(pth, os.O_APPEND|os.O_CREATE|os.O_WRONLY, filePerm) + } + if err != nil { + // failed to create or open-for-append the file + return err + } + defer func() { + if err := file.Close(); err != nil { + log.Println(" [!] Failed to close file:", err) + } + }() + + if _, err := file.Write(fileCont); err != nil { + return err + } + + return nil +} + +// ReadBytesFromFile ... +func ReadBytesFromFile(pth string) ([]byte, error) { + if isExists, err := pathutil.IsPathExists(pth); err != nil { + return []byte{}, err + } else if !isExists { + return []byte{}, fmt.Errorf("No file found at path: %s", pth) + } + + bytes, err := ioutil.ReadFile(pth) + if err != nil { + return []byte{}, err + } + return bytes, nil +} + +// ReadStringFromFile ... +func ReadStringFromFile(pth string) (string, error) { + contBytes, err := ReadBytesFromFile(pth) + if err != nil { + return "", err + } + return string(contBytes), nil +} + +// GetFileModeOfFile ... +// this is the "permissions" info, which can be passed directly to +// functions like WriteBytesToFileWithPermission or os.OpenFile +func GetFileModeOfFile(pth string) (os.FileMode, error) { + finfo, err := os.Lstat(pth) + if err != nil { + return 0, err + } + return finfo.Mode(), nil +} + +// GetFilePermissions ... +// - alias of: GetFileModeOfFile +// this is the "permissions" info, which can be passed directly to +// functions like WriteBytesToFileWithPermission or os.OpenFile +func GetFilePermissions(filePth string) (os.FileMode, error) { + return GetFileModeOfFile(filePth) +} diff --git a/vendor/github.com/bitrise-tools/xcode-project/serialized/error.go b/vendor/github.com/bitrise-tools/xcode-project/serialized/error.go new file mode 100644 index 0000000..ea63009 --- /dev/null +++ b/vendor/github.com/bitrise-tools/xcode-project/serialized/error.go @@ -0,0 +1,54 @@ +package serialized + +import "fmt" + +// KeyNotFoundError ... +type KeyNotFoundError struct { + key string + object Object +} + +// Error ... +func (e KeyNotFoundError) Error() string { + return fmt.Sprintf("key: %T(%#v) not found in: %T(%#v)", e.key, e.key, e.object, e.object) +} + +// NewKeyNotFoundError ... +func NewKeyNotFoundError(key string, object Object) KeyNotFoundError { + return KeyNotFoundError{key: key, object: object} +} + +// IsKeyNotFoundError ... +func IsKeyNotFoundError(err error) bool { + if err == nil { + return false + } + _, ok := err.(KeyNotFoundError) + return ok +} + +// TypeCastError ... +type TypeCastError struct { + key string + value interface{} + expectedType string +} + +// NewTypeCastError ... +func NewTypeCastError(key string, value interface{}, expected interface{}) TypeCastError { + return TypeCastError{key: key, value: value, expectedType: fmt.Sprintf("%T", expected)} +} + +// IsTypeCastError ... +func IsTypeCastError(err error) bool { + if err == nil { + return false + } + _, ok := err.(TypeCastError) + return ok +} + +// Error ... +func (e TypeCastError) Error() string { + return fmt.Sprintf("value: %T(%#v) for key: %T(%#v) can not be casted to: %s", e.value, e.value, e.key, e.key, e.expectedType) +} diff --git a/vendor/github.com/bitrise-tools/xcode-project/serialized/serialized.go b/vendor/github.com/bitrise-tools/xcode-project/serialized/serialized.go new file mode 100644 index 0000000..96c8a29 --- /dev/null +++ b/vendor/github.com/bitrise-tools/xcode-project/serialized/serialized.go @@ -0,0 +1,77 @@ +package serialized + +// Object ... +type Object map[string]interface{} + +// Keys ... +func (o Object) Keys() []string { + var keys []string + for key := range o { + keys = append(keys, key) + } + return keys +} + +// Value ... +func (o Object) Value(key string) (interface{}, error) { + value, ok := o[key] + if !ok { + return nil, NewKeyNotFoundError(key, o) + } + return value, nil +} + +// String ... +func (o Object) String(key string) (string, error) { + value, err := o.Value(key) + if err != nil { + return "", err + } + + casted, ok := value.(string) + if !ok { + return "", NewTypeCastError(key, value, "") + } + + return casted, nil +} + +// StringSlice ... +func (o Object) StringSlice(key string) ([]string, error) { + value, err := o.Value(key) + if err != nil { + return nil, err + } + + casted, ok := value.([]interface{}) + if !ok { + return nil, NewTypeCastError(key, value, []interface{}{}) + } + + slice := []string{} + for _, v := range casted { + item, ok := v.(string) + if !ok { + return nil, NewTypeCastError(key, casted, "") + } + + slice = append(slice, item) + } + + return slice, nil +} + +// Object ... +func (o Object) Object(key string) (Object, error) { + value, err := o.Value(key) + if err != nil { + return nil, err + } + + casted, ok := value.(map[string]interface{}) + if !ok { + return nil, NewTypeCastError(key, value, map[string]interface{}{}) + } + + return casted, nil +}