diff --git a/cmd/holotreeHash.go b/cmd/holotreeHash.go index d545161..a1e2660 100644 --- a/cmd/holotreeHash.go +++ b/cmd/holotreeHash.go @@ -2,8 +2,12 @@ package cmd import ( "fmt" + "io" + "os" + "strings" "github.com/robocorp/rcc/common" + "github.com/robocorp/rcc/conda" "github.com/robocorp/rcc/htfs" "github.com/robocorp/rcc/operations" "github.com/robocorp/rcc/pretty" @@ -12,7 +16,8 @@ import ( ) var ( - holotreeShowBlueprint bool + holotreeShowBlueprint bool + holotreeReadInputFromStdin bool ) var holotreeHashCmd = &cobra.Command{ @@ -24,8 +29,31 @@ var holotreeHashCmd = &cobra.Command{ if common.DebugFlag() { defer common.Stopwatch("Conda YAML hash calculation lasted").Report() } - _, holotreeBlueprint, err := htfs.ComposeFinalBlueprint(args, "", common.DevDependencies) - pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) + + var err error + if holotreeReadInputFromStdin { + // Now, read from stdin until EOF and use that as the input to calculate the blueprint hash. + data, err := io.ReadAll(os.Stdin) + pretty.Guard(err == nil, 1, "Failed to read from stdin: %v", err) + + if len(args) != 1 { + common.Fatal("When reading from stdin, the target file path must be provided as the first (and only) argument.", nil) + } + + filename := args[0] + if !strings.HasSuffix(filename, ".yaml") { + common.Fatal("When reading from stdin, the target file path must be provided as the first (and only) argument and it must be a `.yaml` file.", nil) + } + + environment, err := conda.ReadPackageCondaYamlFromContents(data, filename, common.DevDependencies) + pretty.Guard(err == nil, 1, "Failed to read from stdin: %v (filename: %q)", err, filename) + + holotreeBlueprint, err = htfs.BlueprintFromEnvironment(environment) + pretty.Guard(err == nil, 1, "Failed to calculate blueprint from environment: %v", err) + } else { + _, holotreeBlueprint, err = htfs.ComposeFinalBlueprint(args, "", common.DevDependencies) + pretty.Guard(err == nil, 1, "Blueprint calculation failed: %v", err) + } hash := common.BlueprintHash(holotreeBlueprint) common.Log("Blueprint hash for %v is %v.", args, hash) @@ -59,4 +87,5 @@ func init() { holotreeHashCmd.Flags().BoolVarP(&common.DevDependencies, "devdeps", "", false, "Include dev-dependencies from the `package.yaml` when calculating the hash (only valid when dealing with a `package.yaml` file).") holotreeHashCmd.Flags().BoolVarP(&holotreeJson, "json", "j", false, "Show environment as JSON.") holotreeHashCmd.Flags().BoolVarP(&holotreeShowBlueprint, "show-blueprint", "", false, "Show full blueprint, not just hash.") + holotreeHashCmd.Flags().BoolVarP(&holotreeReadInputFromStdin, "stdin", "", false, "Read the conda.yaml/package.yaml contents from stdin.") } diff --git a/common/version.go b/common/version.go index f29db10..97c7cca 100644 --- a/common/version.go +++ b/common/version.go @@ -1,5 +1,5 @@ package common const ( - Version = `v18.4.0` + Version = `v18.5.0` ) diff --git a/conda/condayaml.go b/conda/condayaml.go index 04b2316..d0a1241 100644 --- a/conda/condayaml.go +++ b/conda/condayaml.go @@ -2,11 +2,9 @@ package conda import ( "fmt" - "os" "regexp" "strings" - "github.com/robocorp/rcc/cloud" "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" @@ -658,21 +656,6 @@ func CondaYamlFrom(content []byte) (*Environment, error) { return result.AsEnvironment(), nil } -func readCondaYaml(filename string) (*Environment, error) { - var content []byte - var err error - - if pathlib.IsFile(filename) { - content, err = os.ReadFile(filename) - } else { - content, err = cloud.ReadFile(filename) - } - if err != nil { - return nil, fmt.Errorf("%q: %w", filename, err) - } - return CondaYamlFrom(content) -} - func pipContent(result []*Dependency, value interface{}) []*Dependency { values, ok := value.([]interface{}) if !ok { diff --git a/conda/packageyaml.go b/conda/packageyaml.go index 64ed746..615b0bd 100644 --- a/conda/packageyaml.go +++ b/conda/packageyaml.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/robocorp/rcc/cloud" - "github.com/robocorp/rcc/common" "github.com/robocorp/rcc/pathlib" "gopkg.in/yaml.v2" ) @@ -90,25 +89,19 @@ func packageYamlFrom(content []byte, devDependencies bool) (*Environment, error) return result.AsEnvironment(devDependencies), nil } -func ReadPackageCondaYaml(filename string, devDependencies bool) (*Environment, error) { +func ReadPackageCondaYamlFromContents(content []byte, filename string, devDependencies bool) (*Environment, error) { basename := strings.ToLower(filepath.Base(filename)) if basename == "package.yaml" { - environment, err := readPackageYaml(filename, devDependencies) - if err == nil { - return environment, nil - } + return packageYamlFrom(content, devDependencies) } if devDependencies { // error: only valid when dealing with a `package.yaml` file return nil, fmt.Errorf("'--devdeps' flag is only valid when dealing with a `package.yaml` file. Current file: %q", filename) } - return readCondaYaml(filename) + return CondaYamlFrom(content) } -func readPackageYaml(filename string, devDependencies bool) (*Environment, error) { - if devDependencies { - common.Debug("Reading file %q with dev dependencies", filename) - } +func ReadPackageCondaYaml(filename string, devDependencies bool) (*Environment, error) { var content []byte var err error @@ -120,5 +113,6 @@ func readPackageYaml(filename string, devDependencies bool) (*Environment, error if err != nil { return nil, fmt.Errorf("%q: %w", filename, err) } - return packageYamlFrom(content, devDependencies) + + return ReadPackageCondaYamlFromContents(content, filename, devDependencies) } diff --git a/docs/changelog.md b/docs/changelog.md index 4edcd06..c26c9fa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # rcc change log +## v18.5.0 (date: 25.10.2024) + +- support `--stdin` flag in `rcc holotree hash` to read the package file content from stdin + (note that the actual package file path still is expected to be given as an + argument, but its contents will be read from stdin instead). + ## v18.4.0 (date: 25.10.2024) - support `--show-blueprint` flag in `rcc holotree hash` to show blueprint. diff --git a/htfs/commands.go b/htfs/commands.go index 4d3093b..fbcd0bc 100644 --- a/htfs/commands.go +++ b/htfs/commands.go @@ -270,12 +270,17 @@ func ComposeFinalBlueprint(userFiles []string, packfile string, devDependencies fail.On(err != nil, "Failure: %v", err) } fail.On(right == nil, "Missing environment specification(s).") - content, err := right.AsYaml() - fail.On(err != nil, "YAML error: %v", err) - blueprint = []byte(strings.TrimSpace(content)) + blueprint, err = BlueprintFromEnvironment(right) + fail.On(err != nil, "Blueprint from environment error: %v", err) if !right.IsCacheable() { fingerprint := common.BlueprintHash(blueprint) pretty.Warning("Holotree blueprint %q is not publicly cacheable. Use `rcc robot diagnostics` to find out more.", fingerprint) } return config, blueprint, nil } + +func BlueprintFromEnvironment(environment *conda.Environment) ([]byte, error) { + content, err := environment.AsYaml() + fail.On(err != nil, "YAML error: %v", err) + return []byte(strings.TrimSpace(content)), nil +} diff --git a/robot_tests/__init__.robot b/robot_tests/__init__.robot index 8cd35f3..1146ec9 100644 --- a/robot_tests/__init__.robot +++ b/robot_tests/__init__.robot @@ -1,5 +1,5 @@ *** Settings *** Resource resources.robot -Suite Setup Prepare Local -Suite Teardown Clean Local +Suite Setup Prepare Local +Suite Teardown Clean Local diff --git a/robot_tests/expected/ht_hash/ht_hash_txt_conda.txt b/robot_tests/expected/ht_hash/ht_hash_txt_conda.txt new file mode 100644 index 0000000..3c332bc --- /dev/null +++ b/robot_tests/expected/ht_hash/ht_hash_txt_conda.txt @@ -0,0 +1 @@ +Blueprint hash for [robot_tests/conda.yaml] is 79ba58276b645f09. \ No newline at end of file diff --git a/robot_tests/ht_hash.robot b/robot_tests/ht_hash.robot index 91711ad..034a4f6 100644 --- a/robot_tests/ht_hash.robot +++ b/robot_tests/ht_hash.robot @@ -49,3 +49,45 @@ Goal: Check that hash command works correctly with devdeps ... ... robot_tests/expected/ht_hash/ht_hash_devdeps_txt.txt ... use_stream=stderr + +Goal: Check that hash command works reading from stdin + Run And Check Expected Output + ... build/rcc holotree hash --json --show-blueprint --devdeps --controller citests --stdin package.yaml + ... + ... robot_tests/expected/ht_hash/ht_hash_devdeps_json_show.txt + ... pass_file_as_stdin=robot_tests/bare_action/package.yaml + + Run And Check Expected Output + ... build/rcc holotree hash --json --devdeps --controller citests --stdin package.yaml + ... + ... robot_tests/expected/ht_hash/ht_hash_devdeps_json.txt + ... pass_file_as_stdin=robot_tests/bare_action/package.yaml + + Run And Check Expected Output + ... build/rcc holotree hash --show-blueprint --devdeps --controller citests --stdin robot_tests/bare_action/package.yaml + ... + ... robot_tests/expected/ht_hash/ht_hash_devdeps_txt_show.txt + ... use_stream=stderr + ... pass_file_as_stdin=robot_tests/bare_action/package.yaml + + Run And Check Expected Output + ... build/rcc holotree hash --devdeps --controller citests --stdin robot_tests/bare_action/package.yaml + ... + ... robot_tests/expected/ht_hash/ht_hash_devdeps_txt.txt + ... use_stream=stderr + ... pass_file_as_stdin=robot_tests/bare_action/package.yaml + + # Check conda with stdin in the same + Run And Check Expected Output + ... build/rcc holotree hash --controller citests --stdin robot_tests/conda.yaml + ... + ... robot_tests/expected/ht_hash/ht_hash_txt_conda.txt + ... use_stream=stderr + ... pass_file_as_stdin=robot_tests/conda.yaml + + # Check conda without stdin in the same + Run And Check Expected Output + ... build/rcc holotree hash --controller citests robot_tests/conda.yaml + ... + ... robot_tests/expected/ht_hash/ht_hash_txt_conda.txt + ... use_stream=stderr diff --git a/robot_tests/supporting.py b/robot_tests/supporting.py index 5169617..fa1e54b 100644 --- a/robot_tests/supporting.py +++ b/robot_tests/supporting.py @@ -2,6 +2,7 @@ import logging import subprocess import sys +from pathlib import Path log = logging.getLogger(__name__) @@ -61,6 +62,7 @@ def run_and_return_code_output_error( env: dict[str, str] | None = None, cwd: str | None = None, check: bool = False, + stdin_contents: str | None = None, ) -> tuple[int, str, str]: command = fix_command(command) cwd = get_cwd() if cwd is None else cwd @@ -71,10 +73,13 @@ def run_and_return_code_output_error( shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, + stdin=subprocess.PIPE if stdin_contents else None, cwd=cwd, env=env, ) - out, err = task.communicate() + out, err = task.communicate( + input=stdin_contents.encode("utf-8") if stdin_contents else None + ) if check: assert ( task.returncode == 0 @@ -114,11 +119,17 @@ def normalize_output(output: str) -> str: def run_and_check_expected_output( - command: str, expected_output: str, expected_file: str, use_stream: str = "stdout" + command: str, + expected_output: str, + expected_file: str, + use_stream: str = "stdout", + pass_file_as_stdin: Path | None = None, ): - from pathlib import Path + stdin_contents = pass_file_as_stdin.read_text() if pass_file_as_stdin else None - ret, out, err = run_and_return_code_output_error(command) + ret, out, err = run_and_return_code_output_error( + command, stdin_contents=stdin_contents + ) assert ( ret == 0 ), f"Unexpected exit code {ret!r} from {command!r} with output: {out!r} and error: {err!r}"