diff --git a/.drone.yml b/.drone.yml index 1b5426b097..e79097e55a 100644 --- a/.drone.yml +++ b/.drone.yml @@ -112,3 +112,83 @@ steps: commands: - wget -qO- https://github.com/fossas/fossa-cli/releases/download/v1.0.11/fossa-cli_1.0.11_linux_amd64.tar.gz | tar xvz -C /go/bin/ - /go/bin/fossa test + +--- +kind: pipeline +type: docker +name: release + +platform: + os: linux + arch: amd64 + +trigger: + event: + include: + - tag + +steps: +- name: license-scan + image: golang:1.13 + environment: + FOSSA_API_KEY: + from_secret: fossa_api_key + commands: + - wget -qO- https://github.com/fossas/fossa-cli/releases/download/v1.0.11/fossa-cli_1.0.11_linux_amd64.tar.gz | tar xvz -C /go/bin/ + - /go/bin/fossa analyze + +- name: build + image: golang:1.13 + commands: + - make ci + +- name: lint + image: golangci/golangci-lint:v1.21.0 + commands: + - golangci-lint run + +- name: license-check + image: golang:1.13 + environment: + FOSSA_API_KEY: + from_secret: fossa_api_key + commands: + - wget -qO- https://github.com/fossas/fossa-cli/releases/download/v1.0.11/fossa-cli_1.0.11_linux_amd64.tar.gz | tar xvz -C /go/bin/ + - /go/bin/fossa test + +- name: create-dist + image: golang:1.13 + commands: + - go run tools/create-artifacts/main.go -version ${DRONE_TAG} -commit ${DRONE_COMMIT} -goversion `go version | awk '{print $$3}'` + +- name: publish + image: plugins/github-release + settings: + api_key: + from_secret: github_token + files: dist/* + note: changelog/NOTE.md + +- name: docker-reva-tag + pull: always + image: plugins/docker + settings: + repo: cs3org/reva + tags: ${DRONE_TAG} + dockerfile: Dockerfile.reva + username: + from_secret: dockerhub_username + password: + from_secret: dockerhub_password + +- name: docker-revad-tag + pull: always + image: plugins/docker + settings: + repo: cs3org/revad + tags: ${DRONE_TAG} + dockerfile: Dockerfile.revad + username: + from_secret: dockerhub_username + password: + from_secret: dockerhub_password diff --git a/.gitignore b/.gitignore index bb3c81e533..c5a436c23f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ cmd/revad/revad cmd/revad/revad.pid cmd/reva/reva +tools/release/release # Ignore pid files *.pid @@ -33,3 +34,4 @@ docs/node_modules/ docs/tech-doc-hugo docs/themes/ +dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..2fa2706a4a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +Changelog for Reva unreleased (UNRELEASED) +======================================= + +The following sections list the changes in Reva unreleased relevant to +Reva users. The changes are ordered by importance. + +Summary +------- + + * Enh #243: Create reference when accepting a share + +Details +------- + + * Enhancement #243: Create reference when accepting a share + + When accepting a share only the state information changed for the share was not merged into the + user tree. Now after accepting a share, the share is mounted into the user tree in a location + specified by the user. + + https://github.com/reva/reva/pull/243 + + diff --git a/Makefile b/Makefile index 287931639f..2741024a1a 100644 --- a/Makefile +++ b/Makefile @@ -7,28 +7,30 @@ GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD` GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "dirty-"` VERSION=`git describe --always` GO_VERSION=`go version | awk '{print $$3}'` -BUILD_PLATFORM=`go version | awk '{print $$4}'` - -LDFLAGS=-ldflags "-s -X main.buildDate=${BUILD_DATE} -X main.gitCommit=${GIT_DIRTY}${GIT_COMMIT} -X main.gitBranch=${GIT_BRANCH} -X main.version=${VERSION} -X main.goVersion=${GO_VERSION} -X main.buildPlatform=${BUILD_PLATFORM}" off: GORPOXY=off + echo BUILD_DATE=${BUILD_DATE} + echo GIT_COMMIT=${GIT_COMMIT} + echo GIT_DIRTY=${GIT_DIRTY} + echo VERSION=${VERSION} + echo GO_VERSION=${GO_VERSION} imports: off goimports -w tools pkg internal cmd build: imports - go build -mod=vendor -o ./cmd/revad/revad ${LDFLAGS} ./cmd/revad - go build -mod=vendor -o ./cmd/reva/reva ${LDFLAGS} ./cmd/reva + go build -mod=vendor -o ./cmd/revad/revad ./cmd/revad + go build -mod=vendor -o ./cmd/reva/reva ./cmd/reva tidy: go mod tidy build-revad: imports - go build -mod=vendor -o ./cmd/revad/revad ${LDFLAGS} ./cmd/revad + go build -mod=vendor -o ./cmd/revad/revad ./cmd/revad build-reva: imports - go build -mod=vendor -o ./cmd/reva/reva ${LDFLAGS} ./cmd/reva + go build -mod=vendor -o ./cmd/reva/reva ./cmd/reva test: off go test -mod=vendor -race ./... @@ -42,12 +44,13 @@ contrib: # for manual building only deps: - cd /tmp && go get github.com/golangci/golangci-lint/cmd/golangci-lint + cd /tmp && rm -rf golangci-lint && git clone --quiet -b 'v1.21.0' --single-branch --depth 1 https://github.com/golangci/golangci-lint &> /dev/null && cd golangci-lint/cmd/golangci-lint && go install cd /tmp && go get golang.org/x/tools/cmd/goimports build-ci: off - go build -mod=vendor -o ./cmd/revad/revad ${LDFLAGS} ./cmd/revad - go build -mod=vendor -o ./cmd/reva/reva ${LDFLAGS} ./cmd/reva + go build -mod=vendor -o ./cmd/revad/revad ./cmd/revad + go build -mod=vendor -o ./cmd/reva/reva ./cmd/reva + lint-ci: go run tools/check-license/check-license.go @@ -57,7 +60,19 @@ ci: build-ci test lint-ci # to be run in Docker build build-revad-docker: off - go build -mod=vendor -o ./cmd/revad/revad ${LDFLAGS} ./cmd/revad + go build -mod=vendor -o ./cmd/revad/revad ./cmd/revad build-reva-docker: off - go build -mod=vendor -o ./cmd/revad/reva ${LDFLAGS} ./cmd/reva + go build -mod=vendor -o ./cmd/revad/reva ./cmd/reva +clean: + rm -rf dist + +# for releasing you need to run: go run tools/prepare-release/main.go +# $ go run tools/prepare-release/main.go -version 0.0.1 -commit -tag +release-deps: + cd /tmp && go get github.com/restic/calens + +# create local build versions +dist: default + go run tools/create-artifacts/main.go -version ${VERSION} -commit ${GIT_COMMIT} -goversion ${GO_VERSION} +all: deps default diff --git a/changelog/CHANGELOG.tmpl b/changelog/CHANGELOG.tmpl new file mode 100644 index 0000000000..1997f6caaf --- /dev/null +++ b/changelog/CHANGELOG.tmpl @@ -0,0 +1,32 @@ +{{- range $changes := . }}{{ with $changes -}} +Changelog for Reva {{ .Version }} ({{ .Date }}) +======================================= + +The following sections list the changes in Reva {{ .Version }} relevant to +Reva users. The changes are ordered by importance. + +Summary +------- +{{ range $entry := .Entries }}{{ with $entry }} + * {{ .TypeShort }} #{{ .PrimaryID }}: {{ .Title }} +{{- end }}{{ end }} + +Details +------- +{{ range $entry := .Entries }}{{ with $entry }} + * {{ .Type }} #{{ .PrimaryID }}: {{ .Title }} +{{ range $par := .Paragraphs }} + {{ wrap $par 80 3 }} +{{ end -}} +{{ range $url := .IssueURLs }} + {{ $url -}} +{{ end -}} +{{ range $url := .PRURLs }} + {{ $url -}} +{{ end -}} +{{ range $url := .OtherURLs }} + {{ $url -}} +{{ end }} +{{ end }}{{ end }} + +{{ end }}{{ end -}} diff --git a/changelog/TEMPLATE b/changelog/TEMPLATE new file mode 100644 index 0000000000..12c0b439b7 --- /dev/null +++ b/changelog/TEMPLATE @@ -0,0 +1,14 @@ +Bugfix: Fix behavior for foobar (in present tense) + +We've fixed the behavior for foobar, a long-standing annoyance for Reva +users. + +The text in the paragraphs is written in past tense. The last section is a list +of issue URLs, PR URLs and other URLs. The first issue ID (or the first PR ID, +in case there aren't any issue links) is used as the primary ID. + +https://github.com/cs3org/reva/issues/292 +https://github.com/cs3org/reva/pull/323 + +Note: you can use Bugfix or Enhancement as the first keyword to denote +either a bufix or an improvement. diff --git a/changelog/changelog-github.tmpl b/changelog/changelog-github.tmpl new file mode 100644 index 0000000000..00bfe94dc8 --- /dev/null +++ b/changelog/changelog-github.tmpl @@ -0,0 +1,31 @@ +{{- range $changes := . }}{{ with $changes -}} +Changelog for Reva {{ .Version }} ({{ .Date }}) +======================================= + +The following sections list the changes in Reva {{ .Version }} relevant to Reva users. The changes are ordered by importance. + +Summary +------- +{{ range $entry := .Entries }}{{ with $entry }} + * {{ .TypeShort }} [#{{ .PrimaryID }}]({{ .PrimaryURL }}): {{ .Title }} +{{- end }}{{ end }} + +Details +------- +{{ range $entry := .Entries }}{{ with $entry }} + * {{ .Type }} #{{ .PrimaryID }}: {{ .Title }} +{{ range $par := .Paragraphs }} + {{ $par }} +{{ end }} + {{ range $id := .Issues -}} +{{ ` ` }}[#{{ $id }}](https://github.com/reva/reva/issues/{{ $id -}}) +{{- end -}} +{{ range $id := .PRs -}} +{{ ` ` }}[#{{ $id }}](https://github.com/reva/reva/pull/{{ $id -}}) +{{- end -}} +{{ ` ` }}{{ range $url := .OtherURLs -}} +{{ $url -}} +{{- end }} +{{ end }}{{ end }} + +{{ end }}{{ end -}} diff --git a/changelog/unreleased/pull-334 b/changelog/unreleased/pull-334 new file mode 100644 index 0000000000..cf7048b1b7 --- /dev/null +++ b/changelog/unreleased/pull-334 @@ -0,0 +1,8 @@ +Enhancement: Create release procedure for Reva + +Reva did not have any procedure to release versions. +This PR brings a new tool to release Reva versions (tools/release) +and prepares the necessary files for artefact distributed made from Drone +into Github pages. + +https://github.com/reva/reva/pull/334 diff --git a/cmd/reva/main.go b/cmd/reva/main.go index 501c5d5ad0..cbac495e92 100644 --- a/cmd/reva/main.go +++ b/cmd/reva/main.go @@ -27,8 +27,7 @@ import ( var ( conf *config - // Compile time variables - gitCommit, gitBranch, buildDate, version, goVersion, buildPlatform string + gitCommit, buildDate, version, goVersion string ) func main() { diff --git a/cmd/reva/version.go b/cmd/reva/version.go index f338fcab79..4e2e4c2a70 100644 --- a/cmd/reva/version.go +++ b/cmd/reva/version.go @@ -28,12 +28,10 @@ var versionCommand = func() *command { cmd.Action = func() error { msg := "version=%s " msg += "commit=%s " - msg += "branch=%s " msg += "go_version=%s " - msg += "build_date=%s " - msg += "build_platform=%s\n" + msg += "build_date=%s\n" - fmt.Printf(msg, version, gitCommit, gitBranch, goVersion, buildDate, buildPlatform) + fmt.Printf(msg, version, gitCommit, goVersion, buildDate) return nil } return cmd diff --git a/cmd/revad/main.go b/cmd/revad/main.go index 3d343c8a4b..d7cbbb94d4 100644 --- a/cmd/revad/main.go +++ b/cmd/revad/main.go @@ -53,7 +53,7 @@ var ( pidFlag = flag.String("p", "", "pid file. If empty defaults to a random file in the OS temporary directory") // Compile time variables initialez with gcc flags. - gitCommit, gitBranch, buildDate, version, goVersion, buildPlatform string + gitCommit, buildDate, version, goVersion string ) type coreConf struct { @@ -190,12 +190,10 @@ func getWriter(out string) (io.Writer, error) { func getVersionString() string { msg := "version=%s " msg += "commit=%s " - msg += "branch=%s " msg += "go_version=%s " - msg += "build_date=%s " - msg += "build_platform=%s" + msg += "build_date=%s" - return fmt.Sprintf(msg, version, gitCommit, gitBranch, goVersion, buildDate, buildPlatform) + return fmt.Sprintf(msg, version, gitCommit, goVersion, buildDate) } func handleVersionFlag() { diff --git a/docs/content/en/docs/Changelog/_index.md b/docs/content/en/docs/Changelog/_index.md new file mode 100644 index 0000000000..5a6b3b6d92 --- /dev/null +++ b/docs/content/en/docs/Changelog/_index.md @@ -0,0 +1,8 @@ +--- +title: "Changelog" +linkTitle: "Changelog" +weight: 40 +description: > + Changelog of Reva releases +--- + diff --git a/go.mod b/go.mod index 67d82cf8ea..eecc06d2bd 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,6 @@ require ( github.com/pkg/errors v0.8.1 github.com/pkg/xattr v0.4.1 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect - github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 // indirect github.com/rs/cors v1.7.0 github.com/rs/zerolog v1.15.0 go.opencensus.io v0.22.1 diff --git a/tools/create-artifacts/main.go b/tools/create-artifacts/main.go new file mode 100644 index 0000000000..44cd067598 --- /dev/null +++ b/tools/create-artifacts/main.go @@ -0,0 +1,118 @@ +// Copyright 2018-2019 CERN +// +// 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. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "time" +) + +var ( + commit = flag.String("commit", "", "sets git commit") + version = flag.String("version", "", "sets git version") + goVersion = flag.String("goversion", "", "sets go version") + buildDate = time.Now().Format("2006-01-02") + + binaries = []string{"reva", "revad"} + archs = []string{"386", "amd64"} + oses = []string{"linux", "darwin"} +) + +func init() { + flag.Parse() + + if *commit == "" || *version == "" || *goVersion == "" { + fmt.Fprint(os.Stderr, "fill all the flags\n") + os.Exit(1) + } +} + +func main() { + + if err := os.RemoveAll("dist"); err != nil { + fmt.Fprintf(os.Stderr, "error removing dist folder: %s", err) + os.Exit(1) + } + + if err := os.MkdirAll("dist", 0755); err != nil { + fmt.Fprintf(os.Stderr, "error creating dist folder: %s", err) + os.Exit(1) + } + + ldFlags := fmt.Sprintf("-s -X main.buildDate=%s -X main.gitCommit=%s -X main.version=%s -X main.goVersion=%s", + buildDate, + *commit, + *version, + *goVersion, + ) + + for _, bin := range binaries { + for _, o := range oses { + for _, arch := range archs { + out := fmt.Sprintf("./dist/%s_%s_%s_%s", bin, *version, o, arch) + args := []string{"build", "-mod=vendor", "-o", out, "-ldflags", ldFlags, "./cmd/" + bin} + cmd := exec.Command("go", args...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, []string{"GOOS=" + o, "GOARCH=" + arch}...) + cmd.Dir = "." // root of the repo + run(cmd) + hashFile(out) + } + } + } +} + +func run(cmd *exec.Cmd) { + var b bytes.Buffer + mw := io.MultiWriter(os.Stdout, &b) + cmd.Stdout = mw + cmd.Stderr = mw + err := cmd.Run() + fmt.Println(cmd.Dir, cmd.Args) + fmt.Println(b.String()) + if err != nil { + fmt.Println("ERROR: ", err.Error()) + os.Exit(1) + } +} + +func hashFile(file string) { + hasher := sha256.New() + f, err := os.Open(file) + if err != nil { + log.Fatal(err) + } + defer f.Close() + if _, err := io.Copy(hasher, f); err != nil { + log.Fatal(err) + } + val := hex.EncodeToString(hasher.Sum(nil)) + if err := ioutil.WriteFile(file+".sha256", []byte(val), 0644); err != nil { + log.Fatal(err) + } +} diff --git a/tools/prepare-release/main.go b/tools/prepare-release/main.go new file mode 100644 index 0000000000..bc2e3820eb --- /dev/null +++ b/tools/prepare-release/main.go @@ -0,0 +1,235 @@ +// Copyright 2018-2019 CERN +// +// 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. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path" + "strings" + "time" +) + +var ( + version = flag.String("version", "", "version to release: 0.0.1") + commit = flag.Bool("commit", false, "creates a commit") + tag = flag.Bool("tag", false, "creates a tag") +) + +func init() { + flag.Parse() + + if *version == "" { + fmt.Fprintf(os.Stderr, "missing version: use -version flag\n") + os.Exit(1) + } +} + +func main() { + // check if repo is dirty + if isRepoDirty() { + fmt.Fprintf(os.Stderr, "the repo is dirty, to generate a new release all changes need to be committed\n") + os.Exit(1) + return + } + + // also the build is okay + cmd := exec.Command("make", "all") + run(cmd) + + fmt.Printf("Generating new release: version=%s\n", *version) + + dt := time.Now() + date := dt.Format("2006-01-02") + newChangelog := fmt.Sprintf("changelog/%s_%s", *version, date) + + if info, _ := os.Stat("changelog/unreleased"); info == nil { + fmt.Fprintf(os.Stderr, "no changelog/unreleased folder, to create a new version you need to fill it") + os.Exit(1) + } + + cmd = exec.Command("mv", "changelog/unreleased", newChangelog) + run(cmd) + + // install release-deps: calens + cmd = exec.Command("make", "release-deps") + run(cmd) + + // create new changelog + cmd = exec.Command("calens", "-o", "CHANGELOG.md") + run(cmd) + + // add new VERSION and BUILD_DATE + if err := ioutil.WriteFile("VERSION", []byte(*version), 0644); err != nil { + fmt.Fprintf(os.Stderr, "error writing to VERSION file: %s", err) + os.Exit(1) + } + + // add new VERSION and RELEASE_DATE + if err := ioutil.WriteFile("RELEASE_DATE", []byte(date), 0644); err != nil { + fmt.Fprintf(os.Stderr, "error writing to RELEASE_DATE file: %s", err) + os.Exit(1) + } + + tmp, err := ioutil.TempDir("", "reva-changelog") + if err != nil { + fmt.Fprintf(os.Stderr, "error creating tmp directory to store changelog: %s", err) + os.Exit(1) + } + defer os.RemoveAll(tmp) + + if err := os.MkdirAll(path.Join(tmp, "changelog"), 0755); err != nil { + fmt.Fprintf(os.Stderr, "error creating changelog in temporary directory: %s", tmp) + os.Exit(1) + } + + dir := path.Join(tmp, fmt.Sprintf("changelog/%s_%s", *version, date)) + cmd = exec.Command("cp", "-a", fmt.Sprintf("changelog/%s_%s", *version, date), dir) + run(cmd) + + // create new changelog + cmd = exec.Command("calens", "-o", "changelog/NOTE.md", "-i", path.Join(tmp, "changelog")) + run(cmd) + + // Generate changelog also in the documentation + if err := os.MkdirAll(fmt.Sprintf("docs/content/en/docs/Changelog/%s", *version), 0755); err != nil { + fmt.Fprintf(os.Stderr, "error creating docs/content/en/docs/Changelog/%s: %s", *version, err) + os.Exit(1) + } + + data, err := ioutil.ReadFile("changelog/NOTE.md") + if err != nil { + fmt.Fprintf(os.Stderr, "error reading NOTE.md: %s", err) + os.Exit(1) + } + + releaseDocs := fmt.Sprintf(` +--- +title: "v%s" +linkTitle: "v%s" +weight: 40 +description: > + Changelog for Reva v%s (%s) +--- + +`, *version, *version, *version, date) + + releaseDocs += string(data) + if err := ioutil.WriteFile(fmt.Sprintf("docs/content/en/docs/Changelog/%s/_index.md", *version), []byte(releaseDocs), 0644); err != nil { + fmt.Fprintf(os.Stderr, "error writing docs release file _index.md: %s", err) + os.Exit(1) + } + + add(fmt.Sprintf("v%s", *version), + "changelog", + "CHANGELOG.md", + "VERSION", + "RELEASE_DATE", + "docs/content/en/docs/Changelog", + ) + + if *commit { + createCommit(fmt.Sprintf("v%s", *version)) + fmt.Println("Commit created, check with git log") + } + + if *tag { + createTag(*version) + fmt.Println("Tag created, check with git tag") + } + + if *tag && *commit { + fmt.Println("RELEASE READY: you only need to\n$ git push --follow-tags") + os.Exit(0) + } else { + fmt.Println("Was a dry run, run with -commit and -tag to create release") + os.Exit(1) + } +} + +func isRepoDirty() bool { + repo := "." + cmd := exec.Command("git", "status", "-s") + cmd.Dir = repo + changes := runAndGet(cmd) + if changes != "" { + fmt.Println("repo is dirty") + fmt.Println(changes) + } + return changes != "" +} + +func add(msg string, files ...string) { + for _, f := range files { + cmd := exec.Command("git", "add", f) + cmd.Dir = "." + run(cmd) + } + +} + +func createCommit(msg string) { + cmd := exec.Command("git", "commit", "-m", msg) + cmd.Dir = "." // always run from the root of the repo + run(cmd) +} + +func createTag(version string) { + // check if repo is dirty + if isRepoDirty() { + fmt.Fprintf(os.Stderr, "repo is dirty when creating a new tag for version %s", version) + os.Exit(1) + } + + cmd := exec.Command("git", "tag", "-a", "v"+version, "-m", "v"+version) + run(cmd) +} + +func run(cmd *exec.Cmd) { + var b bytes.Buffer + mw := io.MultiWriter(os.Stdout, &b) + cmd.Stdout = mw + cmd.Stderr = mw + err := cmd.Run() + fmt.Println(cmd.Dir, cmd.Args) + fmt.Println(b.String()) + if err != nil { + fmt.Println("ERROR: ", err.Error()) + os.Exit(1) + } +} + +func runAndGet(cmd *exec.Cmd) string { + var b bytes.Buffer + mw := io.MultiWriter(os.Stdout, &b) + cmd.Stderr = mw + out, err := cmd.Output() + fmt.Println(cmd.Dir, cmd.Args) + fmt.Println(b.String()) + if err != nil { + fmt.Println("ERROR: ", err.Error()) + os.Exit(1) + } + return strings.TrimSpace(string(out)) +}