diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..403cec6 --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,24 @@ +Want to contribute? Great! First, read this page (including the small print at the end). +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement] +(https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. +### Code reviews +All submissions, including submissions by project members, require review. We +use Github pull requests for this purpose. +### The small print +Contributions made by corporations are covered by a different agreement than +the one above, the +[Software Grant and Corporate Contributor License Agreement] +(https://cla.developers.google.com/about/google-corporate). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..71eaffc --- /dev/null +++ b/LICENSE @@ -0,0 +1,169 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + END OF TERMS AND CONDITIONS + APPENDIX: How to apply the Apache License to your work. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + Copyright [yyyy] [name of copyright owner] + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..92b256d --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# GooGet +GooGet (Googet's Obviously Only a Goofy Experimental Title) is a modular +package repository solution primarily designed for Windows. + +This is not an official Google product. + +## Build +Run build.cmd/build.sh to build GooGet for Windows. To package googet run + +``` +go run goopack/goopack.go googet.goospec +``` + +This will result in googet.x86_64.VERSION.goo which can be installed on a +machine with the `googet install` command (assuming googet is already +installed). + +To install on a fresh machine copy both googet.exe and the googet package +over and run: + +``` +googet -root 'c:/ProgramData/GooGet' install googet googet.x86_64.VERSION.goo +``` + diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..b4c0c9f --- /dev/null +++ b/build.cmd @@ -0,0 +1 @@ +go build -ldflags '-X main.version=1.6.0@1' diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..378bc6c --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +#! /bin/sh +GOOS=windows go build -ldflags '-X main.version=1.6.0@1' diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..f05be3d --- /dev/null +++ b/client/client.go @@ -0,0 +1,298 @@ +/* +Copyright 2016 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 client contains common functions for the GooGet client. +package client + +import ( + "compress/gzip" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/google/googet/goolib" + "github.com/google/logger" +) + +// PackageState describes the state of a package on a client. +type PackageState struct { + SourceRepo, DownloadURL, Checksum, UnpackDir string + PackageSpec *goolib.PkgSpec + InstalledFiles map[string]string +} + +// GooGetState describes the overall package state on a client. +type GooGetState []PackageState + +// Add appends a PackageState. +func (s *GooGetState) Add(ps PackageState) { + *s = append(*s, ps) +} + +// Remove removes a PackageState. +func (s *GooGetState) Remove(pi goolib.PackageInfo) error { + for i, ps := range *s { + if ps.Match(pi) { + (*s)[i] = (*s)[len(*s)-1] + *s = (*s)[:len(*s)-1] + return nil + } + } + return fmt.Errorf("no match found for package %s.%s.%s in state", pi.Name, pi.Arch, pi.Ver) +} + +// GetPackageState returns the PackageState of the matching goolib.PackageInfo, +// or error if no match is found. +func (s *GooGetState) GetPackageState(pi goolib.PackageInfo) (PackageState, error) { + for _, ps := range *s { + if ps.Match(pi) { + return ps, nil + } + } + return PackageState{}, fmt.Errorf("no match found for package %s.%s.%s", pi.Name, pi.Arch, pi.Ver) +} + +// Marshal JSON marshals GooGetState. +func (s *GooGetState) Marshal() ([]byte, error) { + return json.Marshal(s) +} + +// UnmarshalState unmarshals data into GooGetState. +func UnmarshalState(b []byte) (*GooGetState, error) { + var s GooGetState + return &s, json.Unmarshal(b, &s) +} + +// Match reports whether the PackageState corresponds to the package info. +func (ps *PackageState) Match(pi goolib.PackageInfo) bool { + return ps.PackageSpec.Name == pi.Name && (ps.PackageSpec.Arch == pi.Arch || pi.Arch == "") && (ps.PackageSpec.Version == pi.Ver || pi.Ver == "") +} + +// RepoMap describes each repo's packages as seen from a client. +type RepoMap map[string][]goolib.RepoSpec + +// AvailableVersions builds a RepoMap from a list of sources. +func AvailableVersions(srcs []string, cacheDir string, cacheLife time.Duration) RepoMap { + rm := make(RepoMap) + for _, r := range srcs { + rf, err := unmarshalRepoPackages(r, cacheDir, cacheLife) + if err != nil { + logger.Errorf("error reading repo %q: %v", r, err) + continue + } + rm[r] = rf + } + return rm +} + +func decode(res *http.Response, cf string) ([]goolib.RepoSpec, error) { + ct := res.Header.Get("content-type") + var dec *json.Decoder + switch ct { + case "application/gzip": + gr, err := gzip.NewReader(res.Body) + if err != nil { + return nil, err + } + dec = json.NewDecoder(gr) + case "application/json": + dec = json.NewDecoder(res.Body) + default: + return nil, fmt.Errorf("unsupported content type: %s", ct) + } + var m []goolib.RepoSpec + for dec.More() { + if err := dec.Decode(&m); err != nil { + return nil, err + } + } + + f, err := os.Create(cf) + if err != nil { + return nil, err + } + j, err := json.Marshal(m) + if err != nil { + return nil, err + } + if _, err := f.Write(j); err != nil { + return nil, err + } + + return m, f.Close() +} + +// unmarshalRepoPackages gets and unmarshals a repository URL or uses the cached contents +// if mtime is less than cacheLife. +// Sucessfully unmarshalled contents will be written to a cache. +func unmarshalRepoPackages(p, cacheDir string, cacheLife time.Duration) ([]goolib.RepoSpec, error) { + cf := filepath.Join(cacheDir, filepath.Base(p)+".rs") + fi, err := os.Stat(cf) + if err == nil && time.Since(fi.ModTime()) < cacheLife { + logger.Infof("Using cached repo content for %s.", p) + f, err := os.Open(cf) + if err != nil { + return nil, err + } + var m []goolib.RepoSpec + dec := json.NewDecoder(f) + for dec.More() { + if err := dec.Decode(&m); err != nil { + return nil, err + } + } + return m, nil + } + if err != nil && !os.IsNotExist(err) { + return nil, err + } + logger.Infof("Fetching repo content for %s, cache either doesn't exist or is older than %v", p, cacheLife) + + url := p + "/index.gz" + logger.Infof("Fetching %q", url) + res, err := http.Get(url) + if err != nil { + return nil, err + } + + if res.StatusCode == 200 { + return decode(res, cf) + } + + logger.Infof("Gzipped index returned status: %q, trying plain JSON.", res.Status) + url = p + "/index" + logger.Infof("Fetching %q", url) + res, err = http.Get(url) + if err != nil { + return nil, err + } + + if res.StatusCode != 200 { + return nil, fmt.Errorf("index GET request returned status: %q", res.Status) + } + + return decode(res, cf) +} + +// FindRepoSpec returns the element of pl whose PackageSpec matches pi. +func FindRepoSpec(pi goolib.PackageInfo, pl []goolib.RepoSpec) (goolib.RepoSpec, error) { + for _, p := range pl { + ps := p.PackageSpec + if ps.Name == pi.Name && ps.Arch == pi.Arch && ps.Version == pi.Ver { + return p, nil + } + } + return goolib.RepoSpec{}, fmt.Errorf("no match found for package %s.%s.%s in repo", pi.Name, pi.Arch, pi.Ver) +} + +func latest(psm map[string][]*goolib.PkgSpec) (ver, repo string) { + for r, pl := range psm { + for _, p := range pl { + if ver == "" { + repo = r + ver = p.Version + continue + } + c, err := goolib.Compare(p.Version, ver) + if err != nil { + logger.Errorf("compare of %s to %s failed with error: %v", p.Version, ver, err) + } + if c == 1 { + repo = r + ver = p.Version + } + } + } + return +} + +// FindRepoLatest returns the latest version of a package along with its repo and arch. +func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (ver, repo, arch string, err error) { + psm := make(map[string][]*goolib.PkgSpec) + if pi.Arch != "" { + for r, pl := range rm { + for _, p := range pl { + if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == pi.Arch { + psm[r] = append(psm[r], p.PackageSpec) + } + } + } + if len(psm) != 0 { + v, r := latest(psm) + return v, r, pi.Arch, nil + } + return "", "", "", fmt.Errorf("no versions of package %s.%s found in any repo", pi.Name, pi.Arch) + } + + for _, a := range archs { + for r, pl := range rm { + for _, p := range pl { + if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == a { + psm[r] = append(psm[r], p.PackageSpec) + } + } + } + if len(psm) != 0 { + v, r := latest(psm) + return v, r, a, nil + } + } + return "", "", "", fmt.Errorf("no versions of package %s found in any repo", pi.Name) +} + +// WhatRepo returns what repo a package is in. +// Name, Arch, and Ver fields of PackageInfo must be provided. +func WhatRepo(pi goolib.PackageInfo, rm RepoMap) (string, error) { + for r, pl := range rm { + for _, p := range pl { + if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == pi.Arch && p.PackageSpec.Version == pi.Ver { + return r, nil + } + } + } + return "", fmt.Errorf("package %s %s version %s not found in any repo", pi.Arch, pi.Name, pi.Ver) +} + +// RemoveOrRename attempts to remove a file or directory. If it fails +// and it's a file, attempt to rename it into a temp file on windows so +// that it can be effectively overridden +func RemoveOrRename(filename string) error { + rmErr := os.Remove(filename) + if rmErr == nil || os.IsNotExist(rmErr) { + return nil + } + fi, err := os.Stat(filename) + if err != nil { + return err + } + if fi.IsDir() { + return rmErr + } + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return err + } + newname := tmpfile.Name() + tmpfile.Close() + if err = os.Remove(newname); err != nil { + return err + } + if err = os.Rename(filename, newname); err != nil { + return err + } + return nil +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..06ff80a --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,311 @@ +/* +Copyright 2016 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 client + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/google/googet/goolib" + "github.com/google/logger" +) + +const ( + cacheLife = 1 * time.Minute + port = 56456 +) + +func init() { + logger.Init("test", true, false, ioutil.Discard) +} + +func TestAppend(t *testing.T) { + s := &GooGetState{} + s.Add(PackageState{SourceRepo: "test"}) + want := &GooGetState{PackageState{SourceRepo: "test"}} + if !reflect.DeepEqual(want, s) { + t.Errorf("Append did not produce expected result, want %+v, got: %+v", want, s) + } +} + +func TestRemove(t *testing.T) { + s := &GooGetState{ + PackageState{PackageSpec: &goolib.PkgSpec{Name: "test"}}, + PackageState{PackageSpec: &goolib.PkgSpec{Name: "test2"}}, + } + if err := s.Remove(goolib.PackageInfo{"test", "", ""}); err != nil { + t.Errorf("error running Remove: %v", err) + } + if len(*s) != 1 { + t.Errorf("Remove did not remove anything, want: len of 1, got: len of %s", len(*s)) + } +} + +func TestRemoveNoMatch(t *testing.T) { + s := &GooGetState{PackageState{PackageSpec: &goolib.PkgSpec{Name: "test2"}}} + if err := s.Remove(goolib.PackageInfo{"test", "", ""}); err == nil { + t.Error("did not get expected error when running Remove") + } +} + +func TestGetPackageState(t *testing.T) { + want := PackageState{PackageSpec: &goolib.PkgSpec{Name: "test"}} + s := &GooGetState{ + want, + PackageState{PackageSpec: &goolib.PkgSpec{Name: "test2"}}, + } + got, err := s.GetPackageState(goolib.PackageInfo{"test", "", ""}) + if err != nil { + t.Errorf("error running GetPackageState: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("GetPackageState did not return expected result, want: %+v, got: %+v", got, want) + } +} + +func TestGetPackageStateNoMatch(t *testing.T) { + s := &GooGetState{PackageState{PackageSpec: &goolib.PkgSpec{Name: "test2"}}} + if _, err := s.GetPackageState(goolib.PackageInfo{"test", "", ""}); err == nil { + t.Error("did not get expected error when running GetPackageState") + } +} + +func TestWhatRepo(t *testing.T) { + rm := RepoMap{ + "foo_repo": []goolib.RepoSpec{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "foo_pkg", + Version: "1.2.3@4", + Arch: "noarch", + }, + }, + }, + } + + got, err := WhatRepo(goolib.PackageInfo{"foo_pkg", "noarch", "1.2.3@4"}, rm) + if err != nil { + t.Fatalf("error running WhatRepo: %v", err) + } + if got != "foo_repo" { + t.Errorf("returned repo does not match expected repo: got %q, want %q", got, "foo_repo") + } +} + +func TestFindRepoLatest(t *testing.T) { + archs := []string{"noarch", "x86_64"} + rm := RepoMap{ + "foo_repo": []goolib.RepoSpec{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "foo_pkg", + Version: "1.2.3@4", + Arch: "noarch", + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "foo_pkg", + Version: "1.0.0@1", + Arch: "noarch", + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "bar_pkg", + Version: "1.0.0@1", + Arch: "noarch", + }, + }, + }, + } + + table := []struct { + pkg string + arch string + wVer string + wArch string + wRepo string + }{ + {"foo_pkg", "noarch", "1.2.3@4", "noarch", "foo_repo"}, + {"foo_pkg", "", "1.2.3@4", "noarch", "foo_repo"}, + } + for _, tt := range table { + gotVer, gotRepo, gotArch, err := FindRepoLatest(goolib.PackageInfo{tt.pkg, tt.arch, ""}, rm, archs) + if err != nil { + t.Fatalf("FindRepoLatest failed: %v", err) + } + if gotVer != tt.wVer { + t.Errorf("FindRepoLatest for %q, %q returned version: %q, want %q", tt.pkg, tt.arch, gotVer, tt.wVer) + } + if gotArch != tt.wArch { + t.Errorf("FindRepoLatest for %q, %q returned arch: %q, want %q", tt.pkg, tt.arch, gotArch, tt.wArch) + } + if gotRepo != tt.wRepo { + t.Errorf("FindRepoLatest for %q, %q returned repo: %q, want %q", tt.pkg, tt.arch, gotRepo, tt.wRepo) + } + } + + werr := "no versions of package bar_pkg.x86_64 found in any repo" + if _, _, _, err := FindRepoLatest(goolib.PackageInfo{"bar_pkg", "x86_64", ""}, rm, archs); err.Error() != werr { + t.Errorf("did not get expected error: got %q, want %q", err, werr) + } +} + +func TestUnmarshalRepoPackagesJSON(t *testing.T) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + want := []goolib.RepoSpec{ + {Source: "foo"}, + {Source: "bar"}, + } + j, err := json.Marshal(want) + if err != nil { + t.Fatalf("Error marshalling json: %v", err) + } + br := bytes.NewReader(j) + + http.HandleFunc("/test-repo/index", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + io.Copy(w, br) + }) + + go http.ListenAndServe(fmt.Sprintf(":%d", port), nil) + + got, err := unmarshalRepoPackages(fmt.Sprintf("http://localhost:%d/test-repo", port), tempDir, cacheLife) + if err != nil { + t.Fatalf("Error running unmarshalRepoPackages: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("unmarshalRepoPackages did not return expected content, got: %+v, want: %+v", got, want) + } +} + +func TestUnmarshalRepoPackagesGzip(t *testing.T) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + want := []goolib.RepoSpec{ + {Source: "foo"}, + {Source: "bar"}, + } + j, err := json.Marshal(want) + if err != nil { + t.Fatalf("Error marshalling json: %v", err) + } + + var b bytes.Buffer + gw := gzip.NewWriter(&b) + if _, err := gw.Write(j); err != nil { + t.Fatal(err) + } + if err := gw.Close(); err != nil { + t.Fatalf("Error closing gzip writer: %v", err) + } + + http.HandleFunc("/test-repo/index.gz", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/gzip") + io.Copy(w, &b) + }) + + go http.ListenAndServe(fmt.Sprintf(":%d", port), nil) + + got, err := unmarshalRepoPackages(fmt.Sprintf("http://localhost:%d/test-repo", port), tempDir, cacheLife) + if err != nil { + t.Fatalf("Error running unmarshalRepoPackages: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("unmarshalRepoPackages did not return expected content, got: %+v, want: %+v", got, want) + } +} + +func TestUnmarshalRepoPackagesCache(t *testing.T) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + want := []goolib.RepoSpec{ + {Source: "foo"}, + {Source: "bar"}, + } + j, err := json.Marshal(want) + if err != nil { + t.Fatalf("Error marshalling json: %v", err) + } + f, err := os.Create(filepath.Join(tempDir, "test-repo.rs")) + if err != nil { + t.Fatalf("Error creating cache file: %v", err) + } + if _, err := f.Write(j); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatalf("Error closing file writer: %v", err) + } + + // No http server as this should use the cached content. + got, err := unmarshalRepoPackages("http://localhost/test-repo", tempDir, cacheLife) + if err != nil { + t.Fatalf("Error running unmarshalRepoPackages: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("unmarshalRepoPackages did not return expected content, got: %+v, want: %+v", got, want) + } +} + +func TestFindRepoSpec(t *testing.T) { + want := goolib.RepoSpec{PackageSpec: &goolib.PkgSpec{Name: "test"}} + rs := []goolib.RepoSpec{ + want, + {PackageSpec: &goolib.PkgSpec{Name: "test2"}}, + } + + got, err := FindRepoSpec(goolib.PackageInfo{"test", "", ""}, rs) + if err != nil { + t.Errorf("error running FindRepoSpec: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("FindRepoSpec did not return expected result, want: %+v, got: %+v", got, want) + } +} + +func TestFindRepoSpecNoMatch(t *testing.T) { + rs := []goolib.RepoSpec{{PackageSpec: &goolib.PkgSpec{Name: "test2"}}} + + if _, err := FindRepoSpec(goolib.PackageInfo{"test", "", ""}, rs); err == nil { + t.Error("did not get expected error when running FindRepoSpec") + } +} diff --git a/download/download.go b/download/download.go new file mode 100644 index 0000000..4559e11 --- /dev/null +++ b/download/download.go @@ -0,0 +1,157 @@ +/* +Copyright 2016 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 download handles the downloading of packages. +package download + +import ( + "archive/tar" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + humanize "github.com/dustin/go-humanize" + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/logger" +) + +// Package downloads a package from the given url, +// if a SHA256 checksum is provided it will be checked. +func Package(pkgURL, dst, chksum string) error { + resp, err := http.Get(pkgURL) + if err != nil { + return err + } + defer resp.Body.Close() + logger.Infof("Downloading %q", pkgURL) + if err := os.RemoveAll(dst); err != nil { + return err + } + if err := download(resp.Body, dst, chksum); err != nil { + return err + } + return nil +} + +// FromRepo downloads a package from a repo. +func FromRepo(rs goolib.RepoSpec, repo, dir string) (string, error) { + pkgURL := strings.TrimSuffix(repo, filepath.Base(repo)) + rs.Source + pn := goolib.PackageInfo{rs.PackageSpec.Name, rs.PackageSpec.Arch, rs.PackageSpec.Version}.PkgName() + dst := filepath.Join(dir, filepath.Base(pn)) + return dst, Package(pkgURL, dst, rs.Checksum) +} + +// Latest downloads the latest available version of a package. +func Latest(name, dir string, rm client.RepoMap, archs []string) (string, error) { + ver, repo, arch, err := client.FindRepoLatest(goolib.PackageInfo{name, "", ""}, rm, archs) + if err != nil { + return "", err + } + rs, err := client.FindRepoSpec(goolib.PackageInfo{name, arch, ver}, rm[repo]) + if err != nil { + return "", err + } + return FromRepo(rs, repo, dir) +} + +func download(r io.Reader, p, chksum string) (err error) { + f, err := os.Create(p) + if err != nil { + return err + } + defer func() { + if cErr := f.Close(); cErr != nil && err == nil { + err = cErr + } + }() + + hash := sha256.New() + tw := io.MultiWriter(f, hash) + + b, err := io.Copy(tw, r) + if err != nil { + return err + } + + logger.Infof("Successfully downloaded %s", humanize.IBytes(uint64(b))) + + if chksum != "" && hex.EncodeToString(hash.Sum(nil)) != chksum { + return errors.New("checksum of downloaded file does not match expected checksum") + } + return nil +} + +// ExtractPkg takes a path to a package and extracts it to a directory based on the +// package name, it returns the path to the extraced directory. +func ExtractPkg(src string) (dst string, err error) { + dst = strings.TrimSuffix(src, filepath.Ext(src)) + if err := os.Mkdir(dst, 0755); err != nil && !os.IsExist(err) { + return "", err + } + logger.Infof("Extracting %q to %q", src, dst) + + f, err := os.Open(src) + if err != nil { + return "", fmt.Errorf("error reading zip package: %v", err) + } + defer f.Close() + + gr, err := gzip.NewReader(f) + if err != nil { + if !os.IsExist(err) { + return "", err + } + } + tr := tar.NewReader(gr) + + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return "", fmt.Errorf("error opening file: %v", err) + } + + path := filepath.Join(dst, header.Name) + if header.FileInfo().IsDir() { + if err := os.MkdirAll(path, 0755); err != nil { + return "", err + } + continue + } + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return "", err + } + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode)) + if err != nil { + return "", err + } + if _, err := io.Copy(f, tr); err != nil { + f.Close() + return "", err + } + if err := f.Close(); err != nil { + return "", err + } + } + return dst, nil +} diff --git a/download/download_test.go b/download/download_test.go new file mode 100644 index 0000000..d6ba6de --- /dev/null +++ b/download/download_test.go @@ -0,0 +1,104 @@ +/* +Copyright 2016 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 download + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" + + "github.com/google/googet/goolib" + "github.com/google/logger" +) + +func init() { + logger.Init("test", true, false, ioutil.Discard) +} + +func TestDownload(t *testing.T) { + r := bytes.NewReader([]byte("some content")) + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("error creating temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + chksum := goolib.Checksum(r) + if _, err := r.Seek(0, 0); err != nil { + t.Errorf("error seeking to front of reader: %v", err) + } + tempFile := path.Join(tempDir, "test") + if err := download(r, tempFile, chksum); err != nil { + t.Errorf("error downloading and checking checksum: %v", err) + } + if err := download(r, tempFile, "notachecksum"); err == nil { + t.Error("wanted but did not recieve checksum error") + } +} + +func TestExtractPkg(t *testing.T) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("error creating temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + tempFile := filepath.Join(tempDir, "test.pkg") + f, err := os.Create(tempFile) + if err != nil { + t.Fatalf("error creating temp file: %v", err) + } + gw := gzip.NewWriter(f) + tw := tar.NewWriter(gw) + + name := "test" + body := "this is a test file" + if err := tw.WriteHeader(&tar.Header{ + Name: name, + Mode: 0600, + Size: int64(len(body)), + }); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte(body)); err != nil { + t.Fatalf("error writing file: %v", err) + } + + if err := tw.Close(); err != nil { + t.Fatalf("error closing tar: %v", err) + } + if err := gw.Close(); err != nil { + t.Fatalf("error closing gzip: %v", err) + } + if err := f.Close(); err != nil { + t.Fatalf("error closing file: %v", err) + } + + dst, err := ExtractPkg(tempFile) + if err != nil { + t.Fatalf("error running ExtractPkg: %v", err) + } + + cts, err := ioutil.ReadFile(filepath.Join(dst, name)) + if err != nil { + t.Fatalf("error opening test file: %v", err) + } + if string(cts) != body { + t.Errorf("contents of extracted file does not match expected contents: got: %q, want: %q", string(cts), body) + } +} diff --git a/googet.go b/googet.go new file mode 100644 index 0000000..8cf24d6 --- /dev/null +++ b/googet.go @@ -0,0 +1,365 @@ +/* +Copyright 2016 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. +*/ + +// The googet binary is the client for the GoGet packaging system, it performs the listing, +// getting, installing and removing functons on client machines. +package main + +import ( + "errors" + "flag" + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "time" + + yaml "github.com/fraenkel/candiedyaml" + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/googet/system" + "github.com/google/logger" + "github.com/google/subcommands" + "github.com/olekukonko/tablewriter" + "golang.org/x/net/context" +) + +const ( + stateFile = "googet.state" + confFile = "googet.conf" + logFile = "googet.log" + lockFile = "googet.lock" + cacheDir = "cache" + repoDir = "repos" + envVar = "GooGetRoot" + logSize = 10 * 1024 * 1024 +) + +var ( + rootDir string + noConfirm bool + verbose bool + systemLog bool + showVer bool + version string + cacheLife = 3 * time.Minute + archs []string +) + +type packageMap map[string]string + +// installedPackages returns a packagemap of all installed packages based on the +// googet state file given. +func installedPackages(state client.GooGetState) packageMap { + pm := make(packageMap) + for _, p := range state { + pm[p.PackageSpec.Name+"."+p.PackageSpec.Arch] = p.PackageSpec.Version + } + return pm +} + +type repoFile struct { + Name string + URL string +} + +func unmarshalRepoFile(p string) (*repoFile, error) { + b, err := ioutil.ReadFile(p) + if err != nil { + return nil, err + } + var rf repoFile + return &rf, yaml.Unmarshal(b, &rf) +} + +type conf struct { + Archs []string + CacheLife string +} + +func unmarshalConfFile(p string) (*conf, error) { + b, err := ioutil.ReadFile(p) + if err != nil { + return nil, err + } + var cf conf + return &cf, yaml.Unmarshal(b, &cf) +} + +func repoList(dir string) ([]string, error) { + fl, err := filepath.Glob(filepath.Join(dir, "*.repo")) + if err != nil { + return nil, err + } + var rl []string + for _, f := range fl { + rf, err := unmarshalRepoFile(f) + if err != nil { + logger.Error(err) + continue + } + rl = append(rl, rf.URL) + } + return rl, nil +} + +func writeState(s *client.GooGetState, sf string) error { + b, err := s.Marshal() + if err != nil { + return err + } + return ioutil.WriteFile(sf, b, 0664) +} + +func readState(sf string) (*client.GooGetState, error) { + b, err := ioutil.ReadFile(sf) + if os.IsNotExist(err) { + logger.Info("No state file found, assuming no packages installed.") + return &client.GooGetState{}, nil + } + if err != nil { + return nil, err + } + return client.UnmarshalState(b) +} + +func buildSources(s string) ([]string, error) { + if s != "" { + srcs := strings.Split(s, ",") + return srcs, nil + } + return repoList(filepath.Join(rootDir, repoDir)) +} + +func confirmation(msg string) bool { + var c string + fmt.Print(msg + " (y/N): ") + fmt.Scanln(&c) + c = strings.ToLower(c) + return c == "y" || c == "yes" +} + +func info(ps *goolib.PkgSpec, r string) { + fmt.Println() + + pkgInfo := []struct { + name, value string + }{ + {"Name", ps.Name}, + {"Arch", ps.Arch}, + {"Version", ps.Version}, + {"Repo", path.Base(r)}, + {"Authors", ps.Authors}, + {"Owners", ps.Owners}, + {"Description", ps.Description}, + {"Dependencies", ""}, + {"ReleaseNotes", ""}, + } + var w int + for _, pi := range pkgInfo { + if len(pi.name) > w { + w = len(pi.name) + } + } + wf := fmt.Sprintf("%%-%vs: %%s\n", w+1) + + for _, pi := range pkgInfo { + if pi.name == "Dependencies" { + var deps []string + for p, v := range ps.PkgDependencies { + deps = append(deps, p+" "+v) + } + if len(deps) == 0 { + fmt.Printf(wf, pi.name, "None") + } else { + fmt.Printf(wf, pi.name, deps[0]) + for _, l := range deps[1:] { + fmt.Printf(wf, "", l) + } + } + } else if pi.name == "ReleaseNotes" && ps.ReleaseNotes != nil { + sl, _ := tablewriter.WrapString(ps.ReleaseNotes[0], 76-w) + fmt.Printf(wf, pi.name, sl[0]) + for _, l := range sl[1:] { + fmt.Printf(wf, "", l) + } + for _, l := range ps.ReleaseNotes[1:] { + sl, _ := tablewriter.WrapString(l, 76-w) + fmt.Printf(wf, "", sl[0]) + for _, l := range sl[1:] { + fmt.Printf(wf, "", l) + } + } + } else { + cl := strings.Split(strings.TrimSpace(pi.value), "\n") + sl, _ := tablewriter.WrapString(cl[0], 76-w) + fmt.Printf(wf, pi.name, sl[0]) + for _, l := range sl[1:] { + fmt.Printf(wf, "", l) + } + for _, l := range cl[1:] { + sl, _ := tablewriter.WrapString(l, 76-w) + for _, l := range sl { + fmt.Printf(wf, "", l) + } + } + } + } +} + +func rotateLog(logPath string, ls int64) error { + fi, err := os.Stat(logPath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if fi.Size() < ls { + return nil + } + oldLog := logPath + ".old" + if err := os.Rename(logPath, oldLog); err != nil { + return fmt.Errorf("error moving log file: %v", err) + } + return nil +} + +func lock(lf string) (*os.File, error) { + // This locking process only works on Windows, on linux os.Remove will remove an open file. + // This is not currently an issue as running googet on linux is only done for testing. + // In the future using a semaphore for locking would be nice. + // 90% of all GooGet runs happen in < 60s, we wait 70s. + for i := 1; i < 15; i++ { + // Try to remove any old lock file that may exist, ignore errors as we don't care if + // we can't remove it or it does not exist. + os.Remove(lf) + if lk, err := os.OpenFile(lf, os.O_RDONLY|os.O_CREATE|os.O_EXCL, 0); err == nil { + return lk, nil + } + if i == 1 { + fmt.Fprintln(os.Stderr, "GooGet lock already held, waiting...") + } + time.Sleep(5 * time.Second) + } + return nil, errors.New("timed out waiting for lock") +} + +func readConf(cf string) { + gc, err := unmarshalConfFile(cf) + if err != nil { + if os.IsNotExist(err) { + gc = &conf{} + } else { + logger.Errorf("Error unmarshalling conf file: %v", err) + } + } + + if gc.Archs != nil { + archs = gc.Archs + } else { + archs, err = system.InstallableArchs() + if err != nil { + logger.Fatal(err) + } + } + + if gc.CacheLife != "" { + cacheLife, err = time.ParseDuration(gc.CacheLife) + if err != nil { + logger.Error(err) + } + } +} + +func run() int { + ggFlags := flag.NewFlagSet(filepath.Base(os.Args[0]), flag.ContinueOnError) + ggFlags.StringVar(&rootDir, "root", os.Getenv(envVar), "googet root directory") + ggFlags.BoolVar(&noConfirm, "noconfirm", false, "skip confirmation") + ggFlags.BoolVar(&verbose, "verbose", false, "print info level logs to stdout") + ggFlags.BoolVar(&systemLog, "system_log", true, "log to Linux Syslog or Windows Event Log") + ggFlags.BoolVar(&showVer, "version", false, "display GooGet version and exit") + + if err := ggFlags.Parse(os.Args[1:]); err != nil && err != flag.ErrHelp { + logger.Fatal(err) + } + + if showVer { + fmt.Println("GooGet version:", version) + os.Exit(0) + } + + cmdr := subcommands.NewCommander(ggFlags, "googet") + cmdr.Register(cmdr.FlagsCommand(), "") + cmdr.Register(cmdr.CommandsCommand(), "") + cmdr.Register(cmdr.HelpCommand(), "") + cmdr.Register(&installCmd{}, "package management") + cmdr.Register(&downloadCmd{}, "package management") + cmdr.Register(&removeCmd{}, "package management") + cmdr.Register(&updateCmd{}, "package management") + cmdr.Register(&installedCmd{}, "package query") + cmdr.Register(&latestCmd{}, "package query") + cmdr.Register(&availableCmd{}, "package query") + + cmdr.ImportantFlag("verbose") + cmdr.ImportantFlag("noconfirm") + + nonLockingCommands := []string{"help", "commands", "flags"} + if ggFlags.NArg() == 0 || goolib.ContainsString(ggFlags.Args()[0], nonLockingCommands) { + return int(cmdr.Execute(context.Background())) + } + + if rootDir == "" { + logger.Fatalf("The environment variable %q not defined and no '-root' flag passed.", envVar) + } + if err := os.MkdirAll(rootDir, 0774); err != nil { + logger.Fatalln("Error setting up root directory:", err) + } + + readConf(filepath.Join(rootDir, confFile)) + + lkf := filepath.Join(rootDir, lockFile) + lk, err := lock(lkf) + if err != nil { + logger.Fatal(err) + } + defer os.Remove(lkf) + defer lk.Close() + + logPath := filepath.Join(rootDir, logFile) + if err := rotateLog(logPath, logSize); err != nil { + logger.Error(err) + } + lf, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0660) + if err != nil { + logger.Fatalln("Failed to open log file:", err) + } + defer lf.Close() + + logger.Init("GooGet", verbose, systemLog, lf) + + if err := os.MkdirAll(filepath.Join(rootDir, cacheDir), 0774); err != nil { + logger.Fatalf("Error setting up cache directory: %v", err) + } + if err := os.MkdirAll(filepath.Join(rootDir, repoDir), 0774); err != nil { + logger.Fatalf("Error setting up repo directory: %v", err) + } + + return int(cmdr.Execute(context.Background())) +} + +func main() { + os.Exit(run()) +} diff --git a/googet.goospec b/googet.goospec new file mode 100644 index 0000000..f3c50c5 --- /dev/null +++ b/googet.goospec @@ -0,0 +1,40 @@ +{ + "name": "googet", + "version": "2.6.0@1", + "arch": "x86_64", + "authors": "ajackura", + "owners": "ajackura", + "description": "GooGet Package Manager", + "files": { + "googet.exe": "/GooGet/googet.exe" + }, + "install": { + "path": "install.ps1" + }, + "releaseNotes": [ + "2.5.3 - COM errors no longer stop GooGet from running.", + "2.5.2 - Switch from Changelog to ReleaseNotes in state file and info.", + "2.5.1 - Subcommands 'help', 'commands' and 'flags' don't require a lock.", + "2.5.0 - Remove deprecated files on upgrade, any files that were referenced in the last package install that are not in the new version will get removed.", + " - Store installed files and their hashes in the state file.", + "2.4.2 - Add additional logging to msi install/uninstalls.", + "2.4.0 - Add googet locking and ability to setup a googet.conf config file.", + "2.3.5 - Add info flag to installed and available commands.", + "2.3.4 - Attempt to redownload packages if unpack directory does not exist.", + "2.3.3 - Resolve environmental variables in Files.", + "2.3.1 - Bug fixes to googet downlad, available and installed filtering.", + "2.3.0 - Enumerate install deps before asking for confirmation.", + "2.2.0 - Move some flags around, add option to apply changes to DB only.", + "2.1.0 - Add ability to install files without a script." + ], + "sources": [{ + "include": [ + "googet.exe", + "install.ps1" + ] + }], + "build": { + "windows": "build.cmd", + "linux": "./build.sh" + } +} diff --git a/googet_available.go b/googet_available.go new file mode 100644 index 0000000..39d5f79 --- /dev/null +++ b/googet_available.go @@ -0,0 +1,110 @@ +/* +Copyright 2016 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 + +// The available subcommand searches the repo for packages using the filter provided. The default +// filter is an empty string and will return all packages. + +import ( + "flag" + "fmt" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/logger" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type availableCmd struct { + filter string + info bool + sources string +} + +func (*availableCmd) Name() string { return "available" } +func (*availableCmd) Synopsis() string { return "List all available packages in repos." } +func (*availableCmd) Usage() string { + return fmt.Sprintf("%s available [-sources repo1,repo2...] [-filter ] [-info]\n", path.Base(os.Args[0])) +} + +func (cmd *availableCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&cmd.filter, "filter", "", "package search filter") + f.BoolVar(&cmd.info, "info", false, "display package info") + f.StringVar(&cmd.sources, "sources", "", "comma separated list of sources, setting this overrides local .repo files") +} + +func (cmd *availableCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + exitCode := subcommands.ExitFailure + + repos, err := buildSources(cmd.sources) + if err != nil { + logger.Fatal(err) + } + if repos == nil { + logger.Fatal("No repos defined, create a .repo file or pass using the -sources flag.") + } + + m := make(map[string][]string) + rm := client.AvailableVersions(repos, filepath.Join(rootDir, cacheDir), cacheLife) + for r, pl := range rm { + for _, p := range pl { + m[r] = append(m[r], p.PackageSpec.Name+"."+p.PackageSpec.Arch+"."+p.PackageSpec.Version) + } + } + + for r, pl := range m { + logger.Infof("Searching %q for packages matching filter %q.", r, cmd.filter) + sort.Strings(pl) + i := sort.SearchStrings(pl, cmd.filter) + if i >= len(pl) || !strings.Contains(pl[i], cmd.filter) { + continue + } + if !cmd.info { + fmt.Println(r) + } + for _, p := range pl { + if strings.Contains(p, cmd.filter) { + exitCode = subcommands.ExitSuccess + pi := goolib.PkgNameSplit(p) + if cmd.info { + repo(pi, rm) + continue + } + fmt.Println(" ", pi.Name+"."+pi.Arch+" "+pi.Ver) + } + } + } + + if exitCode != subcommands.ExitSuccess { + fmt.Fprintf(os.Stderr, "No package matching filter %q available in any repo.\n", cmd.filter) + } + return exitCode +} + +func repo(pi goolib.PackageInfo, rm client.RepoMap) { + for r, pl := range rm { + for _, p := range pl { + if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == pi.Arch && p.PackageSpec.Version == pi.Ver { + info(p.PackageSpec, r) + return + } + } + } +} diff --git a/googet_download.go b/googet_download.go new file mode 100644 index 0000000..f03d52e --- /dev/null +++ b/googet_download.go @@ -0,0 +1,107 @@ +/* +Copyright 2016 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 + +// The download subcommand handles the downloading of a package. + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/google/googet/client" + "github.com/google/googet/download" + "github.com/google/googet/goolib" + "github.com/google/logger" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type downloadCmd struct { + downloadDir string + sources string +} + +func (*downloadCmd) Name() string { return "download" } +func (*downloadCmd) Synopsis() string { return "Download a package." } +func (*downloadCmd) Usage() string { + return fmt.Sprintf("%s download [-sources repo1,repo2...] [-download_dir ] \n", filepath.Base(os.Args[0])) +} + +func (cmd *downloadCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&cmd.downloadDir, "download_dir", "", "directory to download package") + f.StringVar(&cmd.sources, "sources", "", "comma separated list of sources, setting this overrides local .repo files") +} + +func (cmd *downloadCmd) Execute(ctx context.Context, flags *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + if len(flags.Args()) == 0 { + fmt.Fprintf(os.Stderr, "%s\nUsage: %s\n", cmd.Synopsis(), cmd.Usage()) + return subcommands.ExitFailure + } + repos, err := buildSources(cmd.sources) + if err != nil { + logger.Fatal(err) + } + if repos == nil { + logger.Fatal("No repos defined, create a .repo file or pass using the -sources flag.") + } + + rm := client.AvailableVersions(repos, filepath.Join(rootDir, cacheDir), cacheLife) + exitCode := subcommands.ExitSuccess + + dir := cmd.downloadDir + if dir == "" { + dir, err = os.Getwd() + if err != nil { + logger.Fatal(err) + } + } + + for _, arg := range flags.Args() { + pi := goolib.PkgNameSplit(arg) + if pi.Ver == "" { + if _, err := download.Latest(pi.Name, dir, rm, archs); err != nil { + logger.Errorf("error downloading %s, %v", pi.Name, err) + exitCode = subcommands.ExitFailure + } + continue + } + if _, err := goolib.ParseVersion(pi.Ver); err != nil { + logger.Errorf("invalid package version: %q", pi.Ver) + exitCode = subcommands.ExitFailure + continue + } + + repo, err := client.WhatRepo(pi, rm) + if err != nil { + logger.Error(err) + exitCode = subcommands.ExitFailure + continue + } + + rs, err := client.FindRepoSpec(pi, rm[repo]) + if err != nil { + logger.Error(err) + exitCode = subcommands.ExitFailure + continue + } + if _, err := download.FromRepo(rs, repo, dir); err != nil { + logger.Errorf("error downloading %s.%s %s, %v", pi.Name, pi.Arch, pi.Ver, err) + exitCode = subcommands.ExitFailure + continue + } + } + return exitCode +} diff --git a/googet_install.go b/googet_install.go new file mode 100644 index 0000000..d2ba425 --- /dev/null +++ b/googet_install.go @@ -0,0 +1,211 @@ +/* +Copyright 2016 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 + +// The install subcommand handles the downloading and installation of a package. + +import ( + "bytes" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/googet/install" + "github.com/google/logger" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type installCmd struct { + reinstall bool + redownload bool + dbOnly bool + sources string +} + +func (*installCmd) Name() string { return "install" } +func (*installCmd) Synopsis() string { return "Download and install a package and its dependencies." } +func (*installCmd) Usage() string { + return fmt.Sprintf("%s install [-reinstall] [-source repo1,repo2...] \n", filepath.Base(os.Args[0])) +} + +func (cmd *installCmd) SetFlags(f *flag.FlagSet) { + f.BoolVar(&cmd.reinstall, "reinstall", false, "install even if already installed") + f.BoolVar(&cmd.redownload, "redownload", false, "redownload package files") + f.BoolVar(&cmd.dbOnly, "db_only", false, "only make changes to DB, don't perform install system actions") + f.StringVar(&cmd.sources, "sources", "", "comma separated list of sources, setting this overrides local .repo files") +} + +func (cmd *installCmd) Execute(_ context.Context, flags *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + if len(flags.Args()) == 0 { + fmt.Printf("%s\nUsage: %s\n", cmd.Synopsis(), cmd.Usage()) + return subcommands.ExitFailure + } + + if cmd.redownload && !cmd.reinstall { + fmt.Fprintln(os.Stderr, "It's an error to use the -redownload flag without the -reinstall flag") + return subcommands.ExitFailure + } + + args := flags.Args() + exitCode := subcommands.ExitSuccess + + cache := filepath.Join(rootDir, cacheDir) + sf := filepath.Join(rootDir, stateFile) + state, err := readState(sf) + if err != nil { + logger.Fatal(err) + } + + if len(args) == 0 { + return exitCode + } + + repos, err := buildSources(cmd.sources) + if err != nil { + logger.Fatal(err) + } + if repos == nil { + logger.Fatal("No repos defined, create a .repo file or pass using the -sources flag.") + } + + var rm client.RepoMap + for _, arg := range args { + if ext := filepath.Ext(arg); ext == ".goo" { + if !noConfirm { + if base := filepath.Base(arg); !confirmation(fmt.Sprintf("Install %s?", base)) { + fmt.Printf("Not installing %s...\n", base) + continue + } + } + if err := install.FromDisk(arg, cache, state, cmd.dbOnly, cmd.reinstall); err != nil { + logger.Errorf("Error installing %s: %v", arg, err) + exitCode = subcommands.ExitFailure + continue + } + if err := writeState(state, sf); err != nil { + logger.Fatalf("Error writing state file: %v", err) + } + continue + } + + pi := goolib.PkgNameSplit(arg) + if cmd.reinstall { + if err := reinstall(pi, *state, cmd.redownload); err != nil { + logger.Errorf("Error reinstalling %s: %v", pi.Name, err) + exitCode = subcommands.ExitFailure + continue + } + if err := writeState(state, sf); err != nil { + logger.Fatalf("Error writing state file: %v", err) + } + continue + } + if len(rm) == 0 { + rm = client.AvailableVersions(repos, filepath.Join(rootDir, cacheDir), cacheLife) + } + if pi.Ver == "" { + v, _, a, err := client.FindRepoLatest(pi, rm, archs) + pi.Ver, pi.Arch = v, a + if err != nil { + logger.Errorf("Can't resolve version for package %q: %v", pi.Name, err) + exitCode = subcommands.ExitFailure + continue + } + } + if _, err := goolib.ParseVersion(pi.Ver); err != nil { + logger.Errorf("Invalid package version %q: %v", pi.Ver, err) + exitCode = subcommands.ExitFailure + continue + } + + r, err := client.WhatRepo(pi, rm) + if err != nil { + logger.Errorf("Error finding %s.%s.%s in repo: %v", pi.Name, pi.Arch, pi.Ver, err) + exitCode = subcommands.ExitFailure + continue + } + ni, err := install.NeedsInstallation(pi, *state) + if err != nil { + logger.Error(err) + exitCode = subcommands.ExitFailure + continue + } + if !ni { + fmt.Printf("%s.%s.%s or a newer version is already installed on the system\n", pi.Name, pi.Arch, pi.Ver) + continue + } + if !noConfirm { + b, err := enumerateDeps(pi, rm, r, archs, *state) + if err != nil { + logger.Error(err) + exitCode = subcommands.ExitFailure + continue + } + if !confirmation(b.String()) { + fmt.Println("canceling install...") + continue + } + } + if err := install.FromRepo(pi, r, cache, rm, archs, state, cmd.dbOnly); err != nil { + logger.Errorf("Error installing %s.%s.%s: %v", pi.Name, pi.Arch, pi.Ver, err) + exitCode = subcommands.ExitFailure + continue + } + if err := writeState(state, sf); err != nil { + logger.Fatalf("error writing state file: %v", err) + } + } + return exitCode +} + +func reinstall(pi goolib.PackageInfo, state client.GooGetState, rd bool) error { + ps, err := state.GetPackageState(pi) + if err != nil { + return fmt.Errorf("cannot reinstall something that is not already installed") + } + if !noConfirm { + if !confirmation(fmt.Sprintf("Reinstall %s?", pi.Name)) { + fmt.Printf("Not reinstalling %s...\n", pi.Name) + return nil + } + } + if err := install.Reinstall(ps, state, rd); err != nil { + return fmt.Errorf("error reinstalling %s, %v", pi.Name, err) + } + return nil +} + +func enumerateDeps(pi goolib.PackageInfo, rm client.RepoMap, r string, archs []string, state client.GooGetState) (*bytes.Buffer, error) { + dl, err := install.ListDeps(pi, rm, r, archs) + if err != nil { + return nil, fmt.Errorf("error listing dependencies for %s.%s.%s: %v", pi.Name, pi.Arch, pi.Ver, err) + } + var b bytes.Buffer + fmt.Fprintln(&b, "The following packages will be installed:") + for _, di := range dl { + ni, err := install.NeedsInstallation(di, state) + if err != nil { + return nil, err + } + if ni { + fmt.Fprintf(&b, " %s.%s.%s\n", di.Name, di.Arch, di.Ver) + } + } + fmt.Fprintf(&b, "Do you wish to install %s.%s.%s and all dependencies?", pi.Name, pi.Arch, pi.Ver) + return &b, nil +} diff --git a/googet_installed.go b/googet_installed.go new file mode 100644 index 0000000..7585a9f --- /dev/null +++ b/googet_installed.go @@ -0,0 +1,99 @@ +/* +Copyright 2016 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 + +// The installed subcommand lists out all installed packages that match the filter. +// The default filter is an empty string and will return all packages. + +import ( + "flag" + "fmt" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/logger" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type installedCmd struct { + filter string + info bool +} + +func (*installedCmd) Name() string { return "installed" } +func (*installedCmd) Synopsis() string { return "List all installed packages." } +func (*installedCmd) Usage() string { + return fmt.Sprintf("%s installed [-filter ] [-info]\n", path.Base(os.Args[0])) +} + +func (cmd *installedCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&cmd.filter, "filter", "", "package list filter") + f.BoolVar(&cmd.info, "info", false, "display package info") +} + +func (cmd *installedCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + state, err := readState(filepath.Join(rootDir, stateFile)) + if err != nil { + logger.Fatal(err) + } + + pm := installedPackages(*state) + if len(pm) == 0 { + fmt.Println("No packages installed.") + return subcommands.ExitSuccess + } + + var pl []string + for p, v := range pm { + pl = append(pl, p+"."+v) + } + + sort.Strings(pl) + if cmd.filter != "" { + fmt.Printf("Installed packages matching %q:\n", cmd.filter) + } else { + fmt.Println("Installed packages:") + } + exitCode := subcommands.ExitFailure + for _, p := range pl { + if strings.Contains(p, cmd.filter) { + exitCode = subcommands.ExitSuccess + pi := goolib.PkgNameSplit(p) + if cmd.info { + local(pi, *state) + continue + } + fmt.Println(" ", pi.Name+"."+pi.Arch+" "+pi.Ver) + } + } + if exitCode != subcommands.ExitSuccess { + fmt.Fprintf(os.Stderr, "No package matching filter %q installed.\n", cmd.filter) + } + return exitCode +} + +func local(pi goolib.PackageInfo, state client.GooGetState) { + for _, p := range state { + if p.Match(pi) { + info(p.PackageSpec, "installed") + return + } + } +} diff --git a/googet_latest.go b/googet_latest.go new file mode 100644 index 0000000..90d5462 --- /dev/null +++ b/googet_latest.go @@ -0,0 +1,92 @@ +/* +Copyright 2016 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 + +// The latest subcommand searches the repo for the specified package and returns the latest version. + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/logger" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type latestCmd struct { + compare bool + sources string +} + +func (*latestCmd) Name() string { return "latest" } +func (*latestCmd) Synopsis() string { return "Get the latest available version of a package." } +func (*latestCmd) Usage() string { + return fmt.Sprintf("%s latest [-sources repo1,repo2...] [-compare] \n", filepath.Base(os.Args[0])) +} + +func (cmd *latestCmd) SetFlags(f *flag.FlagSet) { + f.BoolVar(&cmd.compare, "compare", false, "compare to version locally installed") + f.StringVar(&cmd.sources, "sources", "", "comma separated list of sources, setting this overrides local .repo files") +} + +func (cmd *latestCmd) Execute(_ context.Context, flags *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + pi := goolib.PkgNameSplit(flags.Arg(0)) + + repos, err := buildSources(cmd.sources) + if err != nil { + logger.Fatal(err) + } + if repos == nil { + logger.Fatal("No repos defined, create a .repo file or pass using the -sources flag.") + } + + rm := client.AvailableVersions(repos, filepath.Join(rootDir, cacheDir), cacheLife) + v, _, a, err := client.FindRepoLatest(pi, rm, archs) + if err != nil { + logger.Fatal(err) + } + if !cmd.compare { + fmt.Println(v) + return subcommands.ExitSuccess + } + + state, err := readState(filepath.Join(rootDir, stateFile)) + if err != nil { + logger.Fatal(err) + } + pi.Arch = a + var ver string + for _, p := range *state { + if p.Match(pi) { + ver = p.PackageSpec.Version + break + } + fmt.Println(v) + return subcommands.ExitSuccess + } + c, err := goolib.Compare(v, ver) + if err != nil { + logger.Fatal(err) + } + if c == -1 { + fmt.Println(ver) + return subcommands.ExitSuccess + } + fmt.Println(v) + return subcommands.ExitSuccess +} diff --git a/googet_remove.go b/googet_remove.go new file mode 100644 index 0000000..c82a74b --- /dev/null +++ b/googet_remove.go @@ -0,0 +1,98 @@ +/* +Copyright 2016 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 + +// The remove subcommand handles the uninstallation of a package. + +import ( + "bytes" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/google/googet/goolib" + "github.com/google/googet/remove" + "github.com/google/logger" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type removeCmd struct { + dbOnly bool +} + +func (cmd *removeCmd) Name() string { return "remove" } +func (cmd *removeCmd) Synopsis() string { return "Uninstall a package." } +func (cmd *removeCmd) Usage() string { + return fmt.Sprintf("%s remove ", os.Args[0]) +} + +func (cmd *removeCmd) SetFlags(f *flag.FlagSet) { + f.BoolVar(&cmd.dbOnly, "db_only", false, "only make changes to DB, don't perform uninstall system actions") +} + +func (cmd *removeCmd) Execute(_ context.Context, flags *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + exitCode := subcommands.ExitSuccess + + sf := filepath.Join(rootDir, stateFile) + state, err := readState(sf) + if err != nil { + logger.Error(err) + } + + for _, arg := range flags.Args() { + pi := goolib.PkgNameSplit(arg) + var ins []string + for _, ps := range *state { + if ps.Match(pi) { + ins = append(ins, ps.PackageSpec.Name+"."+ps.PackageSpec.Arch) + } + } + if len(ins) == 0 { + logger.Errorf("Package %s.%s not installed, cannot remove.", pi.Name, pi.Arch) + continue + } + if len(ins) > 1 { + fmt.Fprintf(os.Stderr, "More than one %s installed, chose one of:\n%s\n", arg, ins) + return subcommands.ExitFailure + } + pi = goolib.PkgNameSplit(ins[0]) + deps, dl := remove.EnumerateDeps(pi, *state) + if !noConfirm { + var b bytes.Buffer + fmt.Fprintln(&b, "The following packages will be removed:") + for _, d := range dl { + fmt.Fprintln(&b, " "+d) + } + fmt.Fprintf(&b, "Do you wish to remove %s and all dependencies?", pi.Name) + if !confirmation(b.String()) { + fmt.Println("canceling removal...") + continue + } + } + fmt.Printf("Removing %s and all dependencies...\n", pi.Name) + if err = remove.All(pi, deps, state, cmd.dbOnly); err != nil { + logger.Errorf("error removing %s, %v", arg, err) + exitCode = subcommands.ExitFailure + continue + } + logger.Infof("Removal of %q and dependant packages completed", pi.Name) + fmt.Printf("Removal of %s completed\n", pi.Name) + if err := writeState(state, sf); err != nil { + logger.Fatalf("error writing state file: %v", err) + } + } + return exitCode +} diff --git a/googet_test.go b/googet_test.go new file mode 100644 index 0000000..6fce08f --- /dev/null +++ b/googet_test.go @@ -0,0 +1,194 @@ +/* +Copyright 2016 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 ( + "io/ioutil" + "os" + "path" + "reflect" + "testing" + "time" + + "github.com/google/googet/client" + "github.com/google/googet/goolib" +) + +func TestRepoList(t *testing.T) { + testRepo := "https://foo.com/googet/bar" + + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("error creating temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + f, err := os.Create(path.Join(tempDir, "test.repo")) + if err != nil { + t.Fatalf("error creating repo file: %v", err) + } + if _, err := f.Write([]byte("url: " + testRepo)); err != nil { + t.Fatalf("error writing repo: %v", err) + } + if err := f.Close(); err != nil { + t.Fatalf("error closing repo file: %v", err) + } + + got, err := repoList(tempDir) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(got, []string{testRepo}) { + t.Errorf("returned repo does not match expected repo: got %v, want %v", got, testRepo) + } +} + +func TestInstalledPackages(t *testing.T) { + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "foo", + Version: "1.2.3@4", + Arch: "noarch", + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "bar", + Version: "0.1.0@1", + Arch: "noarch", + }, + }, + } + + want := packageMap{"foo.noarch": "1.2.3@4", "bar.noarch": "0.1.0@1"} + got := installedPackages(state) + + if !reflect.DeepEqual(got, want) { + t.Errorf("returned map does not match expected map: got %v, want %v", got, want) + } +} + +func TestReadConf(t *testing.T) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("error creating temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + confPath := path.Join(tempDir, "test.conf") + f, err := os.Create(confPath) + if err != nil { + t.Fatalf("error creating conf file: %v", err) + } + + content := []byte("archs: [noarch, x86_64]\ncachelife: 10m") + if _, err := f.Write(content); err != nil { + t.Fatalf("error writing conf file: %v", err) + } + if err := f.Close(); err != nil { + t.Fatalf("error closing conf file: %v", err) + } + + readConf(confPath) + + ea := []string{"noarch", "x86_64"} + if !reflect.DeepEqual(archs, ea) { + t.Errorf("readConf did not create expected arch list, want: %s, got: %s", ea, archs) + } + + ecl := time.Duration(10 * time.Minute) + if cacheLife != ecl { + t.Errorf("readConf did not create expected cacheLife, want: %s, got: %s", ecl, cacheLife) + } +} + +func TestRotateLog(t *testing.T) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("error creating temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + table := []struct { + name string + size int64 + rotated bool + }{ + {"test1.log", 10 * 1024, true}, + {"test2.log", 9 * 1024, false}, + } + + for _, tt := range table { + logPath := path.Join(tempDir, tt.name) + f, err := os.Create(logPath) + if err != nil { + t.Fatalf("error creating log file: %v", err) + } + + if err := f.Truncate(tt.size); err != nil { + t.Fatalf("error truncating log file: %v", err) + } + + if err := f.Close(); err != nil { + t.Fatalf("error closing log file: %v", err) + } + + if err := rotateLog(logPath, 10*1024); err != nil { + t.Errorf("error running rotateLog: %v", err) + } + + switch tt.rotated { + case true: + if _, err := os.Stat(logPath); err == nil { + t.Error("rotateLog did not rotate log as expected, old log file still exists") + } + if _, err := os.Stat(logPath + ".old"); err != nil { + t.Error("rotateLog did not rotate log as expected, .old file does not exist") + } + case false: + if _, err := os.Stat(logPath); err != nil { + t.Error("rotateLog rotated a log we didn't expect") + } + } + } +} + +func TestWriteReadState(t *testing.T) { + want := &client.GooGetState{ + client.PackageState{PackageSpec: &goolib.PkgSpec{Name: "test"}}, + } + + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("error creating temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + sf := path.Join(tempDir, "test.state") + + if err := writeState(want, sf); err != nil { + t.Errorf("error running writeState: %v", err) + } + + got, err := readState(sf) + if err != nil { + t.Errorf("error running readState: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("did not get expected state, got: %+v, want %+v", got, want) + } +} diff --git a/googet_update.go b/googet_update.go new file mode 100644 index 0000000..8392abf --- /dev/null +++ b/googet_update.go @@ -0,0 +1,129 @@ +/* +Copyright 2016 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 + +// The update subcommand handles bulk updating of packages. + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/googet/install" + "github.com/google/logger" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type updateCmd struct { + dbOnly bool + sources string +} + +func (*updateCmd) Name() string { return "update" } +func (*updateCmd) Synopsis() string { return "Update all packages to the latest version available." } +func (*updateCmd) Usage() string { + return fmt.Sprintf("%s update [-sources repo1,repo2...]\n", filepath.Base(os.Args[0])) +} + +func (cmd *updateCmd) SetFlags(f *flag.FlagSet) { + f.BoolVar(&cmd.dbOnly, "db_only", false, "only make changes to DB, don't perform install system actions") + f.StringVar(&cmd.sources, "sources", "", "comma separated list of sources, setting this overrides local .repo files") +} + +func (cmd *updateCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + cache := filepath.Join(rootDir, cacheDir) + sf := filepath.Join(rootDir, stateFile) + state, err := readState(sf) + if err != nil { + logger.Fatal(err) + } + + pm := installedPackages(*state) + if len(pm) == 0 { + fmt.Println("No packages installed.") + return subcommands.ExitSuccess + } + + repos, err := buildSources(cmd.sources) + if err != nil { + logger.Fatal(err) + } + if repos == nil { + logger.Fatal("No repos defined, create a .repo file or pass using the -sources flag.") + } + + rm := client.AvailableVersions(repos, filepath.Join(rootDir, cacheDir), cacheLife) + ud := updates(pm, rm) + if ud == nil { + fmt.Println("No updates available for any installed packages.") + return subcommands.ExitSuccess + } + + if !noConfirm { + if !confirmation("Perform update?") { + fmt.Println("Not updating.") + return subcommands.ExitSuccess + } + } + + exitCode := subcommands.ExitFailure + for _, pi := range ud { + r, err := client.WhatRepo(pi, rm) + if err != nil { + logger.Errorf("Error finding repo: %v.", err) + } + if err := install.FromRepo(pi, r, cache, rm, archs, state, cmd.dbOnly); err != nil { + logger.Errorf("Error updating %s %s %s: %v", pi.Arch, pi.Name, pi.Ver, err) + exitCode = subcommands.ExitFailure + continue + } + } + + if err := writeState(state, sf); err != nil { + logger.Fatalf("Error writing state file: %v", err) + } + + return exitCode +} + +func updates(pm packageMap, rm client.RepoMap) []goolib.PackageInfo { + fmt.Println("Searching for available updates...") + var ud []goolib.PackageInfo + for p, ver := range pm { + pi := goolib.PkgNameSplit(p) + v, r, _, err := client.FindRepoLatest(pi, rm, archs) + if err != nil { + // This error is because this installed package is not available in a repo. + logger.Info(err) + continue + } + c, err := goolib.Compare(v, ver) + if err != nil { + logger.Error(err) + continue + } + if c == 1 { + fmt.Printf(" %s, %s --> %s from %s\n", p, ver, v, r) + logger.Infof("Update for package %s, %s installed and %s available from %s.", p, ver, v, r) + ud = append(ud, goolib.PackageInfo{pi.Name, pi.Arch, v}) + continue + } + logger.Infof("%s - latest version installed", p) + } + return ud +} diff --git a/goolib/goolib.go b/goolib/goolib.go new file mode 100644 index 0000000..a8b9eb8 --- /dev/null +++ b/goolib/goolib.go @@ -0,0 +1,158 @@ +/* +Copyright 2016 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 goolib contains common functions useful when working with GooGet. +package goolib + +import ( + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "syscall" +) + +var interpreter = map[string]string{ + ".ps1": "powershell", + ".cmd": "cmd", + ".bat": "cmd", +} + +// scriptInterpreter reads a scripts extension and returns the interpreter to use. +func scriptInterpreter(s string) (string, error) { + ext := filepath.Ext(s) + itp, ok := interpreter[ext] + if ok { + return itp, nil + } + return "", fmt.Errorf("unknown extension %q", ext) +} + +// Exec execs a script or binary on either Windows or Linux using the provided args. +// The process is successful if the exit code matches any of those provided or '0'. +// stdout and stderr are sent to the writer. +func Exec(s string, args []string, ec []int, w io.Writer) error { + var c *exec.Cmd + switch runtime.GOOS { + case "windows": + cs := filepath.Clean(s) + ipr, err := scriptInterpreter(cs) + if err != nil { + return err + } + switch ipr { + case "powershell": + // We are using `-Command` here instead of `-File` as this catches syntax errors in the script. + args = append([]string{"-ExecutionPolicy", "Bypass", "-NonInteractive", "-NoProfile", "-Command", cs}, args...) + c = exec.Command(ipr, args...) + case "cmd": + c = exec.Command(cs, args...) + default: + return fmt.Errorf("unknown interpreter: %q", ipr) + } + case "linux": + c = exec.Command(s, args...) + default: + return fmt.Errorf("OS %q is not Windows or Linux", runtime.GOOS) + } + return Run(c, ec, w) +} + +// Run runs a command. +// The process is successful if the exit code matches any of those provided or '0'. +// stdout and stderr are sent to the writer and to this process's stdout and stderr. +func Run(c *exec.Cmd, ec []int, w io.Writer) error { + c.Stdout = io.MultiWriter(os.Stdout, w) + c.Stderr = io.MultiWriter(os.Stderr, w) + if err := c.Run(); err != nil { + e, ok := err.(*exec.ExitError) + if !ok { + return err + } + s, ok := e.Sys().(syscall.WaitStatus) + if !ok { + return err + } + if !ContainsInt(s.ExitStatus(), ec) { + return fmt.Errorf("command exited with error code %v", s.ExitStatus()) + } + } + return nil +} + +// PackageInfo describes the name arch and version of a package. +type PackageInfo struct { + Name, Arch, Ver string +} + +// PkgName returns the proper goo package name. +func (pi PackageInfo) PkgName() string { + return fmt.Sprintf("%s.%s.%s.goo", pi.Name, pi.Arch, pi.Ver) +} + +// PkgNameSplit returns the PackageInfo from a package name. +// If the package name does not contain arch or version an empty string +// will be returned. +func PkgNameSplit(pn string) PackageInfo { + pi := strings.SplitN(strings.TrimSpace(pn), ".", 3) + if len(pi) == 2 { + return PackageInfo{pi[0], pi[1], ""} + } + if len(pi) == 3 { + return PackageInfo{pi[0], pi[1], pi[2]} + } + return PackageInfo{pi[0], "", ""} +} + +// Checksum retuns the SHA256 checksum of the provided file. +func Checksum(r io.Reader) string { + hash := sha256.New() + io.Copy(hash, r) + return hex.EncodeToString(hash.Sum(nil)) +} + +// ExtractPkgSpec pulls and unmarshals the package spec file from a +// reader. +func ExtractPkgSpec(r io.Reader) (*PkgSpec, error) { + zr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + return ReadPackageSpec(zr) +} + +// ContainsInt checks if a is in slice. +func ContainsInt(a int, slice []int) bool { + for _, b := range slice { + if a == b { + return true + } + } + return false +} + +// ContainsString checks if a is in slice. +func ContainsString(a string, slice []string) bool { + for _, b := range slice { + if a == b { + return true + } + } + return false +} diff --git a/goolib/goolib_test.go b/goolib/goolib_test.go new file mode 100644 index 0000000..e1afaf2 --- /dev/null +++ b/goolib/goolib_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2016 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 goolib + +import ( + "testing" +) + +func TestScriptInterpreter(t *testing.T) { + table := []struct { + script string + eitp string + }{ + {"/file/path/script.ps1", "powershell"}, + {"/file/path/script.cmd", "cmd"}, + {"/file/path/script.bat", "cmd"}, + } + for _, tt := range table { + itp, err := scriptInterpreter(tt.script) + if err != nil { + t.Errorf("error parsing interpreter: %v", err) + } + if itp != tt.eitp { + t.Errorf("did not get expected interpreter: got %v, want %v", itp, tt.eitp) + } + } +} + +func TestBadScriptInterpreter(t *testing.T) { + if _, err := scriptInterpreter("/file/path/script.ext"); err == nil { + t.Errorf("got no error from scriptInterpreter when processing bad extension, want error") + } + if _, err := scriptInterpreter("/file/path/script"); err == nil { + t.Errorf("got no error from scriptInterpreter when processing no extension, want error") + } +} + +func TestContainsInt(t *testing.T) { + table := []struct { + a int + slice []int + want bool + }{ + {1, []int{1, 2}, true}, + {3, []int{1, 2}, false}, + } + for _, tt := range table { + if got, want := ContainsInt(tt.a, tt.slice), tt.want; got != want { + t.Errorf("Contains(%d, %v) incorrect return: got %v, want %t", tt.a, tt.slice, got, want) + } + } +} + +func TestContainsString(t *testing.T) { + table := []struct { + a string + slice []string + want bool + }{ + {"a", []string{"a", "b"}, true}, + {"c", []string{"a", "b"}, false}, + } + for _, tt := range table { + if got, want := ContainsString(tt.a, tt.slice), tt.want; got != want { + t.Errorf("Contains(%s, %v) incorrect return: got %v, want %t", tt.a, tt.slice, got, want) + } + } +} diff --git a/goolib/goospec.go b/goolib/goospec.go new file mode 100644 index 0000000..2cf4178 --- /dev/null +++ b/goolib/goospec.go @@ -0,0 +1,347 @@ +/* +Copyright 2016 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 goolib + +import ( + "archive/tar" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "github.com/blang/semver" +) + +type build struct { + Windows, Linux string +} + +// PkgSources is a list of includes, excludes and their target in the package. +type PkgSources struct { + Include, Exclude []string + Target, Root string +} + +// GooSpec is the build specification for a package. +type GooSpec struct { + Build build + Sources []PkgSources + PackageSpec *PkgSpec +} + +// RepoSpec is the repository specfication of a package. +type RepoSpec struct { + Checksum, Source string + PackageSpec *PkgSpec +} + +// Marshal returns the formatted RepoSpec. +func (rs *RepoSpec) Marshal() ([]byte, error) { + return json.MarshalIndent(rs, "", " ") +} + +const ( + pkgSpecSuffix = ".pkgspec" + maxTagKeyLen = 127 + maxTagValueSize = 1024 * 10 // 10k +) + +var validArch = []string{"noarch", "x86_64", "x86_32", "arm"} + +// PkgSpec is the internal package specification. +type PkgSpec struct { + Name string + Version string + Arch string + ReleaseNotes []string `json:",omitempty"` + Description string `json:",omitempty"` + License string `json:",omitempty"` + Authors string `json:",omitempty"` + Owners string `json:",omitempty"` + Tags map[string][]byte `json:",omitempty"` + PkgDependencies map[string]string `json:",omitempty"` + Install ExecFile + Verify ExecFile + Uninstall ExecFile + Files map[string]string `json:",omitempty"` +} + +// ExecFile contains info involved in running a script or binary file. +type ExecFile struct { + Path string `json:",omitempty"` + Args []string `json:",omitempty"` + ExitCodes []int `json:",omitempty"` +} + +// Version contains the semver version as well as the GsVer. +// Semver is semantic versioning version. +// GsVer is a GooSpec version number (usually version of installer). +type Version struct { + Semver semver.Version + GsVer int +} + +// Ver returns the goospec version. +func (gs GooSpec) Ver() (Version, error) { + return ParseVersion(gs.PackageSpec.Version) +} + +func (gs GooSpec) verify() error { + return gs.PackageSpec.verify() +} + +// Compare compares string versions of packages v1 to v2: +// -1 == v1 is less than v2 +// 0 == v1 is equal to v2 +// 1 == v1 is greater than v2 +func Compare(v1, v2 string) (int, error) { + pv1, err := ParseVersion(v1) + if err != nil { + return 0, err + } + pv2, err := ParseVersion(v2) + if err != nil { + return 0, err + } + var c int + if c = pv1.Semver.Compare(pv2.Semver); c == 0 { + if pv1.GsVer > pv2.GsVer { + return 1, nil + } + if pv1.GsVer < pv2.GsVer { + return -1, nil + } + return 0, nil + } + return c, nil +} + +func fixVer(ver string) string { + out := []string{"0", "0", "0"} + nums := strings.SplitN(ver, ".", 3) + offset := len(out) - len(nums) + for i, str := range nums { + trimmed := strings.TrimLeft(str, "0") + if trimmed == "" { + trimmed = "0" + } + out[i+offset] = trimmed + } + return strings.Join(out, ".") +} + +// ParseVersion parses the string version into goospec.Version. +func ParseVersion(ver string) (Version, error) { + v := strings.SplitN(ver, "@", 2) + v[0] = fixVer(v[0]) + + sv, err := semver.Parse(v[0]) + if err != nil { + return Version{}, err + } + version := Version{Semver: sv} + if len(v) == 2 { + gv, err := strconv.ParseInt(v[1], 10, 32) + if err != nil { + return version, err + } + version = Version{ + Semver: sv, + GsVer: int(gv), + } + } else { + version = Version{Semver: sv} + } + return version, nil +} + +// Versions contains a list of goospec string versions. +type Versions []string + +func (s Versions) Len() int { + return len(s) +} + +func (s Versions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s Versions) Less(i, j int) bool { + c, err := Compare(s[i], s[j]) + if err != nil { + fmt.Fprintf(os.Stderr, "Compare failed, %q or %s are not a proper version", s[i], s[j]) + } + return c == -1 +} + +// SortVersions sorts a list of goospec string versions. +func SortVersions(versions []string) []string { + var vl []string + for i, v := range versions { + if _, err := ParseVersion(v); err != nil { + fmt.Fprintf(os.Stderr, "Removing %q from list: %v", v, err) + continue + } + vl = append(vl, versions[i]) + } + + sort.Sort(Versions(vl)) + return vl +} + +func unmarshalGooSpec(c []byte) (GooSpec, error) { + var gs GooSpec + //var temp pkgSpec + if err := json.Unmarshal(c, &gs.PackageSpec); err != nil { + return gs, err + } + if err := json.Unmarshal(c, &gs); err != nil { + return gs, err + } + return gs, nil +} + +// ReadGooSpec unmarshalls and verifies a goospec file into the GooSpec struct. +func ReadGooSpec(cf string) (GooSpec, error) { + c, err := ioutil.ReadFile(cf) + if err != nil { + return GooSpec{}, err + } + gs, err := unmarshalGooSpec(c) + if err != nil { + return gs, err + } + if err = gs.verify(); err != nil { + return gs, err + } + return gs, err +} + +// WritePackageSpec takes a PkgSpec and writes it as a JSON file using +// the provided tar writer. +func WritePackageSpec(tw *tar.Writer, spec *PkgSpec) error { + buf := &bytes.Buffer{} + + c, err := MarshalPackageSpec(spec) + if err != nil { + return err + } + buf.Write(c) + + fh := &tar.Header{ + Name: spec.Name + pkgSpecSuffix, + Size: int64(buf.Len()), + ModTime: time.Now(), + Mode: 0644, + } + + if err := tw.WriteHeader(fh); err != nil { + return err + } + if _, err := tw.Write(buf.Bytes()); err != nil { + return err + } + return nil +} + +// ReadPackageSpec reads a PkgSpec from the given reader, which is +// expected to contain an uncompressed tar archive. +func ReadPackageSpec(r io.Reader) (*PkgSpec, error) { + tr := tar.NewReader(r) + for { + header, err := tr.Next() + if err == io.EOF { + return nil, io.ErrUnexpectedEOF + } + if err != nil { + return nil, err + } + if filepath.Ext(header.Name) != pkgSpecSuffix { + continue + } + + data, err := ioutil.ReadAll(tr) + if err != nil { + return nil, err + } + return UnmarshalPackageSpec(data) + } +} + +func (spec *PkgSpec) verify() error { + if spec.Name == "" { + return errors.New("no name defined in package spec") + } + if !ContainsString(spec.Arch, validArch) { + return fmt.Errorf("invalid architecture: %q", spec.Arch) + } + if spec.Version == "" { + return errors.New("Version string empty") + } + if _, err := ParseVersion(spec.Version); err != nil { + return fmt.Errorf("can't parse %q: %v", spec.Version, err) + } + if len(spec.Tags) > 10 { + return errors.New("too many tags") + } + for k, v := range spec.Tags { + if len(k) > maxTagKeyLen { + return errors.New("tag key too large") + } + if len(v) > maxTagValueSize { + return fmt.Errorf("tag %q too large", k) + } + } + for k, v := range spec.PkgDependencies { + if _, err := ParseVersion(v); err != nil { + return fmt.Errorf("can't parse version %q for dependancy %q: %v", v, k, err) + } + } + for src := range spec.Files { + if filepath.IsAbs(src) { + return fmt.Errorf("%q is an absolute path, expected relative", src) + } + } + return nil +} + +// MarshalPackageSpec encodes the given PkgSpec. +func MarshalPackageSpec(spec *PkgSpec) ([]byte, error) { + if err := spec.verify(); err != nil { + return nil, err + } + + return json.MarshalIndent(spec, "", " ") +} + +// UnmarshalPackageSpec parses data and returns a PkgSpec, if it finds +// one. +func UnmarshalPackageSpec(data []byte) (*PkgSpec, error) { + var p PkgSpec + if err := json.Unmarshal(data, &p); err != nil { + return nil, err + } + if err := p.verify(); err != nil { + return nil, err + } + return &p, nil +} diff --git a/goolib/goospec_test.go b/goolib/goospec_test.go new file mode 100644 index 0000000..b4f7b7a --- /dev/null +++ b/goolib/goospec_test.go @@ -0,0 +1,376 @@ +/* +Copyright 2016 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 goolib + +import ( + "archive/tar" + "bytes" + "reflect" + "strings" + "testing" + + "github.com/blang/semver" +) + +func mkVer(maj, min, pat uint64, rel int) Version { + return Version{ + Semver: semver.Version{Major: maj, Minor: min, Patch: pat}, + GsVer: rel, + } +} + +func TestParseVersion(t *testing.T) { + table := []struct { + ver string + res Version + }{ + {"1.2.3@4", mkVer(1, 2, 3, 4)}, + {"1.2.3", mkVer(1, 2, 3, 0)}, + {"1.02.3", mkVer(1, 2, 3, 0)}, + {"1.2@7", mkVer(0, 1, 2, 7)}, + } + for _, tt := range table { + v, err := ParseVersion(tt.ver) + if err != nil { + t.Errorf("error parsing version: %v", err) + } + if !reflect.DeepEqual(v, tt.res) { + t.Errorf("parsed version unexpected: got %v, want %v", v, tt.res) + } + } +} + +func TestBadParseVersion(t *testing.T) { + table := []struct { + ver string + }{ + {"1.2.d3@4"}, + {"1.2.3@4d"}, + {"1.2.3.4@4"}, + } + for _, tt := range table { + if _, err := ParseVersion(tt.ver); err == nil { + t.Error("expected but did not receive version error") + } + } +} + +func TestVerify(t *testing.T) { + gs := GooSpec{ + PackageSpec: &PkgSpec{ + Arch: "noarch", + Name: "name", + Version: "1.2.3@4", + PkgDependencies: map[string]string{"name": "1.2.3@4"}, + }, + } + if err := gs.verify(); err != nil { + t.Error(err) + } +} + +func TestBadVerify(t *testing.T) { + table := []struct { + gs GooSpec + werr string + }{ + {GooSpec{ + PackageSpec: &PkgSpec{ + Arch: "noarch", + Version: "1.2.3@4", + }, + }, "no name defined in package spec"}, + {GooSpec{ + PackageSpec: &PkgSpec{ + Arch: "noarch", + Name: "name", + }, + }, "Version string empty"}, + {GooSpec{ + PackageSpec: &PkgSpec{ + Arch: "noarch", + Name: "name", + Version: "1.2.3:4d", + }, + }, `Invalid character(s) found in patch number "3:4d"`}, + {GooSpec{ + PackageSpec: &PkgSpec{ + Arch: "something", + Name: "name", + Version: "1.2.3@4", + }, + }, `invalid architecture: "something"`}, + {GooSpec{ + PackageSpec: &PkgSpec{ + Arch: "noarch", + Name: "name", + Version: "1.2.3@4", + PkgDependencies: map[string]string{"name": "1.2.3h@4"}, + }, + }, `can't parse version "1.2.3h@4" for dependancy "name": Invalid character(s) found in patch number "3h"`}, + {GooSpec{ + PackageSpec: &PkgSpec{ + Arch: "noarch", + Name: "name", + Version: "1.2.3@4", + Tags: map[string][]byte{ + "a": nil, + "b": nil, + "c": nil, + "d": nil, + "e": nil, + "f": nil, + "g": nil, + "h": nil, + "i": nil, + "j": nil, + "k": nil, + }, + }, + }, "too many tags"}, + {GooSpec{ + PackageSpec: &PkgSpec{ + Arch: "noarch", + Name: "name", + Version: "1.2.3@4", + Tags: map[string][]byte{ + "it little profits that an idle king, by this still hearth, among these barren craigs, I mete and dole unequal laws unto a savage race who hoards, and feeds, and sleeps, and knows not me": []byte("right?"), + }, + }, + }, "tag key too large"}, + {GooSpec{ + PackageSpec: &PkgSpec{ + Arch: "noarch", + Name: "name", + Version: "1.2.3@4", + Tags: map[string][]byte{ + "text": lotsOBytes, + }, + }, + }, `tag "text" too large`}, + } + for _, tt := range table { + err := tt.gs.verify() + if err == nil { + t.Errorf("expected %q, got no error", tt.werr) + continue + } + if !strings.Contains(err.Error(), tt.werr) { + t.Errorf("did not get expected error: got %q, want %q", err.Error(), tt.werr) + } + } +} + +func TestCompare(t *testing.T) { + table := []struct { + v1 string + v2 string + result int + }{ + {"1.2.3@1", "1.2.3@2", -1}, + {"1.2.4@1", "1.2.3@2", 1}, + {"1.2.3", "1.2.3", 0}, + } + for _, tt := range table { + c, err := Compare(tt.v1, tt.v2) + if err != nil { + t.Error(err) + } + if c != tt.result { + t.Errorf("package comparison unexpected result: got %v, want %v", c, tt.result) + } + } +} + +func TestBadCompare(t *testing.T) { + if _, err := Compare("1.2a.3", "1.2.3"); err == nil { + t.Error("expected error, bad semver version") + } +} + +func TestWritePackageSpec(t *testing.T) { + es := &PkgSpec{ + Name: "test", + Version: "1.2.3@4", + Arch: "noarch", + } + + buf := new(bytes.Buffer) + w := tar.NewWriter(buf) + + if err := WritePackageSpec(w, es); err != nil { + t.Errorf("error writing GooSpec: %v", err) + } + if err := w.Close(); err != nil { + t.Errorf("error closing zip writer: %v", err) + } + + spec, err := ReadPackageSpec(buf) + if err != nil { + t.Errorf("ReadPackageSpec: %v", err) + } + + if !reflect.DeepEqual(spec, es) { + t.Errorf("did not get expected spec: got %v, want %v", spec, es) + } +} + +func TestSortVersions(t *testing.T) { + got := SortVersions([]string{"1.2.3@4", "1.5.0", "1.0.0", "1.0", "1.2.A", "1.2.3@1"}) + want := []string{"1.0", "1.0.0", "1.2.3@1", "1.2.3@4", "1.5.0"} + if !reflect.DeepEqual(got, want) { + t.Errorf("did not get expected list: got %v, want %v", got, want) + } +} + +func TestUnmarshalGooSpec(t *testing.T) { + c1 := []byte(`{ + "name": "pkg", + "version": "1.2.3@4", + "arch": "noarch", + "releaseNotes": [ + "1.2.3@4 - something new", + "1.2.3@4 - something" + ], + "description": "blah blah", + "owners": "someone", + "install": { + "path": "install.ps1" + }, + "sources":[ { + "include":["**"], + "root":"some/place" + } ] +}`) + + want := GooSpec{ + Sources: []PkgSources{ + { + Include: []string{"**"}, + Root: "some/place", + }}, + PackageSpec: &PkgSpec{ + Name: "pkg", + Version: "1.2.3@4", + Arch: "noarch", + ReleaseNotes: []string{"1.2.3@4 - something new", "1.2.3@4 - something"}, + Description: "blah blah", + Owners: "someone", + Install: ExecFile{ + Path: "install.ps1", + }, + }, + } + + got, err := unmarshalGooSpec(c1) + if err != nil { + t.Fatalf("error running unmarshalGooSpec: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("did not get expected GooSpec, got: \n%+v\nwant: \n%+v", got, want) + } +} + +func TestMarshal(t *testing.T) { + rs := &RepoSpec{ + Checksum: "asdkgaksd545as4d6", + Source: "blah", + PackageSpec: &PkgSpec{ + Name: "pkg", + Version: "1.2.3@4", + Arch: "noarch", + ReleaseNotes: []string{"1.2.3@4 - something new", "1.2.3@4 - something"}, + Description: "blah blah", + Owners: "someone", + Install: ExecFile{ + Path: "install.ps1", + }, + }, + } + want := []byte(`{ + "Checksum": "asdkgaksd545as4d6", + "Source": "blah", + "PackageSpec": { + "Name": "pkg", + "Version": "1.2.3@4", + "Arch": "noarch", + "ReleaseNotes": [ + "1.2.3@4 - something new", + "1.2.3@4 - something" + ], + "Description": "blah blah", + "Owners": "someone", + "Install": { + "Path": "install.ps1" + }, + "Verify": {}, + "Uninstall": {} + } +}`) + got, err := rs.Marshal() + if err != nil { + t.Fatalf("error running Marshal: %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("did not get expected JSON, got: \n%s\nwant: \n%s", got, want) + } +} + +var lotsOBytes = []byte(`I wish either my father or my mother, or indeed both of them, as they were in duty both equally bound to it, had minded what they were about when they begot me; had they duly consider'd how much depended upon what they were then doing;—that not only the production of a rational Being was concerned in it, but that possibly the happy formation and temperature of his body, perhaps his genius and the very cast of his mind;—and, for aught they knew to the contrary, even the fortunes of his whole house might take their turn from the humours and dispositions which were then uppermost;—Had they duly weighed and considered all this, and proceeded accordingly,—I am verily persuaded I should have made a quite different figure in the world, from that in which the reader is likely to see me.—Believe me, good folks, this is not so inconsiderable a thing as many of you may think it;—you have all, I dare say, heard of the animal spirits, as how they are transfused from father to son, &c. &c.—and a great deal to that purpose:—Well, you may take my word, that nine parts in ten of a man's sense or his nonsense, his successes and miscarriages in this world depend upon their motions and activity, and the different tracks and trains you put them into, so that when they are once set a-going, whether right or wrong, 'tis not a half-penny matter,—away they go cluttering like hey-go mad; and by treading the same steps over and over again, they presently make a road of it, as plain and as smooth as a garden-walk, which, when they are once used to, the Devil himself sometimes shall not be able to drive them off it. + +Pray my Dear, quoth my mother, have you not forgot to wind up the clock?—Good G..! cried my father, making an exclamation, but taking care to moderate his voice at the same time,—Did ever woman, since the creation of the world, interrupt a man with such a silly question? Pray, what was your father saying?—Nothing. + +—Then, positively, there is nothing in the question that I can see, either good or bad.—Then, let me tell you, Sir, it was a very unseasonable question at least,—because it scattered and dispersed the animal spirits, whose business it was to have escorted and gone hand in hand with the Homunculus, and conducted him safe to the place destined for his reception. + +The Homunculus, Sir, in however low and ludicrous a light he may appear, in this age of levity, to the eye of folly or prejudice;—to the eye of reason in scientific research, he stands confess'd—a Being guarded and circumscribed with rights.—The minutest philosophers, who by the bye, have the most enlarged understandings, (their souls being inversely as their enquiries) shew us incontestably, that the Homunculus is created by the same hand,—engender'd in the same course of nature,—endow'd with the same loco-motive powers and faculties with us:—That he consists as we do, of skin, hair, fat, flesh, veins, arteries, ligaments, nerves, cartilages, bones, marrow, brains, glands, genitals, humours, and articulations;—is a Being of as much activity,—and in all senses of the word, as much and as truly our fellow-creature as my Lord Chancellor of England.—He may be benefitted,—he may be injured,—he may obtain redress; in a word, he has all the claims and rights of humanity, which Tully, Puffendorf, or the best ethick writers allow to arise out of that state and relation. + +Now, dear Sir, what if any accident had befallen him in his way alone!—or that through terror of it, natural to so young a traveller, my little Gentleman had got to his journey's end miserably spent;—his muscular strength and virility worn down to a thread;—his own animal spirits ruffled beyond description,—and that in this sad disorder'd state of nerves, he had lain down a prey to sudden starts, or a series of melancholy dreams and fancies, for nine long, long months together.—I tremble to think what a foundation had been laid for a thousand weaknesses both of body and mind, which no skill of the physician or the philosopher could ever afterwards have set thoroughly to rights. + +To my uncle Mr. Toby Shandy do I stand indebted for the preceding anecdote, to whom my father, who was an excellent natural philosopher, and much given to close reasoning upon the smallest matters, had oft, and heavily complained of the injury; but once more particularly, as my uncle Toby well remember'd, upon his observing a most unaccountable obliquity, (as he call'd it) in my manner of setting up my top, and justifying the principles upon which I had done it,—the old gentleman shook his head, and in a tone more expressive by half of sorrow than reproach,—he said his heart all along foreboded, and he saw it verified in this, and from a thousand other observations he had made upon me, That I should neither think nor act like any other man's child:—But alas! continued he, shaking his head a second time, and wiping away a tear which was trickling down his cheeks, My Tristram's misfortunes began nine months before ever he came into the world. + +—My mother, who was sitting by, look'd up, but she knew no more than her backside what my father meant,—but my uncle, Mr. Toby Shandy, who had been often informed of the affair,—understood him very well. + +I know there are readers in the world, as well as many other good people in it, who are no readers at all,—who find themselves ill at ease, unless they are let into the whole secret from first to last, of every thing which concerns you. + +It is in pure compliance with this humour of theirs, and from a backwardness in my nature to disappoint any one soul living, that I have been so very particular already. As my life and opinions are likely to make some noise in the world, and, if I conjecture right, will take in all ranks, professions, and denominations of men whatever,—be no less read than the Pilgrim's Progress itself—and in the end, prove the very thing which Montaigne dreaded his Essays should turn out, that is, a book for a parlour-window;—I find it necessary to consult every one a little in his turn; and therefore must beg pardon for going on a little farther in the same way: For which cause, right glad I am, that I have begun the history of myself in the way I have done; and that I am able to go on, tracing every thing in it, as Horace says, ab Ovo. + +Horace, I know, does not recommend this fashion altogether: But that gentleman is speaking only of an epic poem or a tragedy;—(I forget which,) besides, if it was not so, I should beg Mr. Horace's pardon;—for in writing what I have set about, I shall confine myself neither to his rules, nor to any man's rules that ever lived. + +To such however as do not choose to go so far back into these things, I can give no better advice than that they skip over the remaining part of this chapter; for I declare before-hand, 'tis wrote only for the curious and inquisitive. + +—Shut the door.— + +I was begot in the night betwixt the first Sunday and the first Monday in the month of March, in the year of our Lord one thousand seven hundred and eighteen. I am positive I was.—But how I came to be so very particular in my account of a thing which happened before I was born, is owing to another small anecdote known only in our own family, but now made publick for the better clearing up this point. + +My father, you must know, who was originally a Turkey merchant, but had left off business for some years, in order to retire to, and die upon, his paternal estate in the county of ——, was, I believe, one of the most regular men in every thing he did, whether 'twas matter of business, or matter of amusement, that ever lived. As a small specimen of this extreme exactness of his, to which he was in truth a slave, he had made it a rule for many years of his life,—on the first Sunday-night of every month throughout the whole year,—as certain as ever the Sunday-night came,—to wind up a large house-clock, which we had standing on the back-stairs head, with his own hands:—And being somewhere between fifty and sixty years of age at the time I have been speaking of,—he had likewise gradually brought some other little family concernments to the same period, in order, as he would often say to my uncle Toby, to get them all out of the way at one time, and be no more plagued and pestered with them the rest of the month. + +It was attended but with one misfortune, which, in a great measure, fell upon myself, and the effects of which I fear I shall carry with me to my grave; namely, that from an unhappy association of ideas, which have no connection in nature, it so fell out at length, that my poor mother could never hear the said clock wound up,—but the thoughts of some other things unavoidably popped into her head—& vice versa:—Which strange combination of ideas, the sagacious Locke, who certainly understood the nature of these things better than most men, affirms to have produced more wry actions than all other sources of prejudice whatsoever. + +But this by the bye. + +Now it appears by a memorandum in my father's pocket-book, which now lies upon the table, 'That on Lady-day, which was on the 25th of the same month in which I date my geniture,—my father set upon his journey to London, with my eldest brother Bobby, to fix him at Westminster school;' and, as it appears from the same authority, 'That he did not get down to his wife and family till the second week in May following,'—it brings the thing almost to a certainty. However, what follows in the beginning of the next chapter, puts it beyond all possibility of a doubt. + +—But pray, Sir, What was your father doing all December, January, and February?—Why, Madam,—he was all that time afflicted with a Sciatica. + +On the fifth day of November, 1718, which to the aera fixed on, was as near nine kalendar months as any husband could in reason have expected,—was I Tristram Shandy, Gentleman, brought forth into this scurvy and disastrous world of ours.—I wish I had been born in the Moon, or in any of the planets, (except Jupiter or Saturn, because I never could bear cold weather) for it could not well have fared worse with me in any of them (though I will not answer for Venus) than it has in this vile, dirty planet of ours,—which, o' my conscience, with reverence be it spoken, I take to be made up of the shreds and clippings of the rest;—not but the planet is well enough, provided a man could be born in it to a great title or to a great estate; or could any how contrive to be called up to public charges, and employments of dignity or power;—but that is not my case;—and therefore every man will speak of the fair as his own market has gone in it;—for which cause I affirm it over again to be one of the vilest worlds that ever was made;—for I can truly say, that from the first hour I drew my breath in it, to this, that I can now scarce draw it at all, for an asthma I got in scating against the wind in Flanders;—I have been the continual sport of what the world calls Fortune; and though I will not wrong her by saying, She has ever made me feel the weight of any great or signal evil;—yet with all the good temper in the world I affirm it of her, that in every stage of my life, and at every turn and corner where she could get fairly at me, the ungracious duchess has pelted me with a set of as pitiful misadventures and cross accidents as ever small Hero sustained. + +In the beginning of the last chapter, I informed you exactly when I was born; but I did not inform you how. No, that particular was reserved entirely for a chapter by itself;—besides, Sir, as you and I are in a manner perfect strangers to each other, it would not have been proper to have let you into too many circumstances relating to myself all at once. + +—You must have a little patience. I have undertaken, you see, to write not only my life, but my opinions also; hoping and expecting that your knowledge of my character, and of what kind of a mortal I am, by the one, would give you a better relish for the other: As you proceed farther with me, the slight acquaintance, which is now beginning betwixt us, will grow into familiarity; and that unless one of us is in fault, will terminate in friendship.—O diem praeclarum!—then nothing which has touched me will be thought trifling in its nature, or tedious in its telling. Therefore, my dear friend and companion, if you should think me somewhat sparing of my narrative on my first setting out—bear with me,—and let me go on, and tell my story my own way:—Or, if I should seem now and then to trifle upon the road,—or should sometimes put on a fool's cap with a bell to it, for a moment or two as we pass along,—don't fly off,—but rather courteously give me credit for a little more wisdom than appears upon my outside;—and as we jog on, either laugh with me, or at me, or in short do any thing,—only keep your temper. + +In the same village where my father and my mother dwelt, dwelt also a thin, upright, motherly, notable, good old body of a midwife, who with the help of a little plain good sense, and some years full employment in her business, in which she had all along trusted little to her own efforts, and a great deal to those of dame Nature,—had acquired, in her way, no small degree of reputation in the world:—by which word world, need I in this place inform your worship, that I would be understood to mean no more of it, than a small circle described upon the circle of the great world, of four English miles diameter, or thereabouts, of which the cottage where the good old woman lived is supposed to be the centre?—She had been left it seems a widow in great distress, with three or four small children, in her forty-seventh year; and as she was at that time a person of decent carriage,—grave deportment,—a woman moreover of few words and withal an object of compassion, whose distress, and silence under it, called out the louder for a friendly lift: the wife of the parson of the parish was touched with pity; and having often lamented an inconvenience to which her husband's flock had for many years been exposed, inasmuch as there was no such thing as a midwife, of any kind or degree, to be got at, let the case have been never so urgent, within less than six or seven long miles riding; which said seven long miles in dark nights and dismal roads, the country thereabouts being nothing but a deep clay, was almost equal to fourteen; and that in effect was sometimes next to having no midwife at all; it came into her head, that it would be doing as seasonable a kindness to the whole parish, as to the poor creature herself, to get her a little instructed in some of the plain principles of the business, in order to set her up in it. As no woman thereabouts was better qualified to execute the plan she had formed than herself, the gentlewoman very charitably undertook it; and having great influence over the female part of the parish, she found no difficulty in effecting it to the utmost of her wishes. In truth, the parson join'd his interest with his wife's in the whole affair, and in order to do things as they should be, and give the poor soul as good a title by law to practise, as his wife had given by institution,—he cheerfully paid the fees for the ordinary's licence himself, amounting in the whole, to the sum of eighteen shillings and four pence; so that betwixt them both, the good woman was fully invested in the real and corporal possession of her office, together with all its rights, members, and appurtenances whatsoever. + +These last words, you must know, were not according to the old form in which such licences, faculties, and powers usually ran, which in like cases had heretofore been granted to the sisterhood. But it was according to a neat Formula of Didius his own devising, who having a particular turn for taking to pieces, and new framing over again all kind of instruments in that way, not only hit upon this dainty amendment, but coaxed many of the old licensed matrons in the neighbourhood, to open their faculties afresh, in order to have this wham-wham of his inserted. + +I own I never could envy Didius in these kinds of fancies of his:—But every man to his own taste.—Did not Dr. Kunastrokius, that great man, at his leisure hours, take the greatest delight imaginable in combing of asses tails, and plucking the dead hairs out with his teeth, though he had tweezers always in his pocket? Nay, if you come to that, Sir, have not the wisest of men in all ages, not excepting Solomon himself,—have they not had their Hobby-Horses;—their running horses,—their coins and their cockle-shells, their drums and their trumpets, their fiddles, their pallets,—their maggots and their butterflies?—and so long as a man rides his Hobby-Horse peaceably and quietly along the King's highway, and neither compels you or me to get up behind him,—pray, Sir, what have either you or I to do with it?`) diff --git a/goopack/goopack.go b/goopack/goopack.go new file mode 100644 index 0000000..a55e18d --- /dev/null +++ b/goopack/goopack.go @@ -0,0 +1,397 @@ +/* +Copyright 2016 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. +*/ + +// The goopack binary creates a GooGet package using the provided GooSpec file. +package main + +import ( + "archive/tar" + "compress/gzip" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + + "github.com/google/googet/goolib" +) + +var outputDir = flag.String("output_dir", "", "where to put the built package") + +type fileMap map[string][]string + +// walkDir returns a list of all files in directory and subdirectories, it is similar +// to filepath.Walk but works even if dir is a symlink, which is the case with blaze Filesets. +func walkDir(dir string) ([]string, error) { + rl, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + var wl []string + for _, fi := range rl { + path := filepath.Join(dir, fi.Name()) + + // follow symlinks + if (fi.Mode() & os.ModeSymlink) != 0 { + if fi, err = os.Stat(path); err != nil { + return nil, err + } + } + if !fi.IsDir() { + wl = append(wl, path) + continue + } + l, err := walkDir(path) + if err != nil { + return nil, err + } + wl = append(wl, l...) + } + return wl, nil +} + +// pathMatch is a simpler filepath.Match but which supports recursive globbing +// (**) and doesn't get any more special than * or **. +func pathMatch(pattern, path string) (bool, error) { + regex := []rune("^") + runePattern := []rune(pattern) + for i := 0; i < len(runePattern); i++ { + ch := runePattern[i] + switch ch { + default: + regex = append(regex, ch) + case '%', '\\', '(', ')', '[', ']', '.', '^', '$', '?', '+', '{', '}', '=': + regex = append(regex, '\\', ch) + case '*': + if i+1 < len(runePattern) && runePattern[i+1] == '*' { + if i+2 < len(runePattern) && runePattern[i+2] == '*' { + return false, fmt.Errorf("%s: malformed glob", pattern) + } + regex = append(regex, []rune(".*")...) + i++ + } else { + regex = append(regex, []rune("[^/]*")...) + } + } + } + regex = append(regex, '$') + re, err := regexp.Compile(string(regex)) + if err != nil { + return false, err + } + return re.MatchString(path), nil +} + +func anyMatch(patterns []string, name string) (bool, error) { + for _, ex := range patterns { + m, err := pathMatch(ex, name) + if err != nil { + return false, err + } + if m { + return true, nil + } + } + return false, nil +} + +func min(i, j int) int { + if i < j { + return i + } + return j +} + +type pathWalk struct { + parts [][]string + firstGlob int +} + +// mergeWalks reduces the number of filesystem walks needed. If one walk will +// cover all the paths in another walk, it merges the include patterns, and only +// the larger walk will be performed. +func mergeWalks(walks []pathWalk) []pathWalk { + for i := len(walks) - 2; i >= 0; i-- { + wi := &walks[i] + for j := i + 1; j < len(walks); j++ { + wj := &walks[j] + lowGlob := min(wi.firstGlob, wj.firstGlob) + if lowGlob < 0 { + continue + } + if filepath.Join(wi.parts[0][:lowGlob]...) == filepath.Join(wj.parts[0][:lowGlob]...) { + wi.parts = append(wi.parts, wj.parts...) + wi.firstGlob = lowGlob + if j+1 < len(walks) { + walks = append(walks[:j], walks[j+1:]...) + } else { + walks = walks[:j] + } + } + } + } + return walks +} + +func glob(base string, includes, excludes []string) ([]string, error) { + var pathincludes []string + for _, in := range includes { + pathincludes = append(pathincludes, filepath.Join(base, in)) + } + var pathexcludes []string + for _, ex := range excludes { + pathexcludes = append(pathexcludes, filepath.Join(base, ex)) + } + + var walks []pathWalk + for _, pi := range pathincludes { + parts := [][]string{splitPath(pi)} + if !strings.Contains(pi, "*") { + walks = append(walks, pathWalk{parts, -1}) + continue + } + firstGlob := -1 + for i, part := range parts[0] { + if strings.Contains(part, "*") { + firstGlob = i + break + } + } + walks = append(walks, pathWalk{parts, firstGlob}) + } + + walks = mergeWalks(walks) + + var out []string + for _, walk := range walks { + if walk.firstGlob < 0 { + out = append(out, filepath.Join(walk.parts[0]...)) + continue + } + wd := filepath.Join(walk.parts[0][:walk.firstGlob]...) + files, err := walkDir(wd) + if err != nil { + return nil, fmt.Errorf("walking %s: %v", wd, err) + } + + var walkincludes []string + for _, p := range walk.parts { + walkincludes = append(walkincludes, filepath.Join(p...)) + } + for _, file := range files { + keep, err := anyMatch(walkincludes, file) + if err != nil { + return nil, err + } + remove, err := anyMatch(pathexcludes, file) + if err != nil { + return nil, err + } + if keep && !remove { + out = append(out, file) + } + } + } + return out, nil +} + +func globFiles(s goolib.PkgSources) ([]string, error) { + cr := filepath.Clean(s.Root) + return glob(cr, s.Include, s.Exclude) +} + +func writeFiles(tw *tar.Writer, fm fileMap) error { + for folder, fl := range fm { + for _, file := range fl { + fi, err := os.Stat(file) + if err != nil { + return err + } + fpath := filepath.Join(folder, filepath.Base(file)) + fih, err := tar.FileInfoHeader(fi, "") + if err != nil { + return err + } + fih.Name = fpath + if err := tw.WriteHeader(fih); err != nil { + return err + } + f, err := os.Open(file) + if err != nil { + return err + } + if _, err := io.Copy(tw, f); err != nil { + f.Close() + return err + } + f.Close() + } + } + return nil +} + +func packageFiles(fm fileMap, gs goolib.GooSpec, dir string) (err error) { + pn := goolib.PackageInfo{gs.PackageSpec.Name, gs.PackageSpec.Arch, gs.PackageSpec.Version}.PkgName() + f, err := os.Create(filepath.Join(dir, pn)) + if err != nil { + return err + } + defer func() { + cErr := f.Close() + if cErr != nil && err == nil { + err = cErr + } + }() + gw := gzip.NewWriter(f) + defer func() { + cErr := gw.Close() + if cErr != nil && err == nil { + err = cErr + } + }() + tw := tar.NewWriter(gw) + defer func() { + cErr := tw.Close() + if cErr != nil && err == nil { + err = cErr + } + }() + + if err := writeFiles(tw, fm); err != nil { + return err + } + + return goolib.WritePackageSpec(tw, gs.PackageSpec) +} + +func mapFiles(sources []goolib.PkgSources) (fileMap, error) { + fm := make(fileMap) + for _, s := range sources { + fl, err := globFiles(s) + if err != nil { + return nil, err + } + for _, f := range fl { + dir := strings.TrimPrefix(filepath.Dir(f), s.Root) + // Ensure leading '/' is trimmed for directories. + dir = strings.TrimPrefix(dir, "/") + tgt := filepath.Join(s.Target, dir) + fm[tgt] = append(fm[tgt], f) + } + } + return fm, nil +} + +func splitPath(path string) []string { + parts := strings.Split(filepath.Clean(path), string(os.PathSeparator)) + out := []string{} + for _, part := range parts { + if part != "" { + out = append(out, part) + } + } + if len(path) > 0 && path[0] == '/' { + out = append([]string{"/"}, out...) + } + return out +} + +func verifyFiles(gs goolib.GooSpec, fm fileMap) error { + fs := make(map[string]bool) + for folder, fl := range fm { + parts := splitPath(folder) + for i := range parts { + fs[filepath.Join(parts[:i+1]...)] = true + } + folder = filepath.Join(parts...) + for _, file := range fl { + fpath := filepath.Join(folder, filepath.Base(file)) + fs[fpath] = true + } + } + var missing []string + for src := range gs.PackageSpec.Files { + if !fs[src] { + missing = append(missing, src) + } + } + if len(missing) > 0 { + return fmt.Errorf("requested files %v not in package", missing) + } + return nil +} + +func createPackage(gs goolib.GooSpec, dir string) error { + switch { + case gs.Build.Linux != "" && runtime.GOOS == "linux": + if err := goolib.Exec(gs.Build.Linux, nil, nil, ioutil.Discard); err != nil { + return err + } + case gs.Build.Windows != "" && runtime.GOOS == "windows": + if err := goolib.Exec(gs.Build.Windows, nil, nil, ioutil.Discard); err != nil { + return err + } + } + fm, err := mapFiles(gs.Sources) + if err != nil { + return err + } + if err := verifyFiles(gs, fm); err != nil { + return err + } + return packageFiles(fm, gs, dir) +} + +func usage() { + fmt.Printf("Usage: %s \n", filepath.Base(os.Args[0])) +} + +func main() { + flag.Parse() + switch len(flag.Args()) { + case 0: + fmt.Println("Not enough args.") + usage() + os.Exit(1) + case 1: + default: + fmt.Println("Too many args.") + usage() + os.Exit(1) + } + if flag.Arg(1) == "help" { + usage() + os.Exit(0) + } + dir := *outputDir + if dir == "" { + var err error + dir, err = os.Getwd() + if err != nil { + log.Fatal(err) + } + } + gs, err := goolib.ReadGooSpec(flag.Arg(0)) + if err != nil { + log.Fatal(err) + } + + if err := createPackage(gs, dir); err != nil { + log.Fatal(err) + } +} diff --git a/goopack/goopack_test.go b/goopack/goopack_test.go new file mode 100644 index 0000000..c8fbfdb --- /dev/null +++ b/goopack/goopack_test.go @@ -0,0 +1,164 @@ +/* +Copyright 2016 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 ( + "archive/tar" + "bytes" + "io/ioutil" + "os" + "path" + "reflect" + "testing" + + "github.com/google/googet/goolib" +) + +func TestPathMatch(t *testing.T) { + tests := []struct { + pattern, path string + result bool + }{ + {"/path**.file", "/path/to.file", true}, + {"/path[a-z]", "/pathb", false}, + {"/path[a-z]", "/path[a-z]", true}, + {"path/*/file", "path/to/file", true}, + {"path/*/file", "path/to/the/file", false}, + {"path/**/file", "path/to/the/file", true}, + {"^$[a(-z])%{}}\\{{\\", "^$[a(-z])%{}}\\{{\\", true}, + } + + for _, test := range tests { + res, err := pathMatch(test.pattern, test.path) + if err != nil { + t.Fatalf("match %q %q: %v", test.pattern, test.path, err) + } + if res != test.result { + t.Fatalf("match %q %q: expected %v got %v", test.pattern, test.path, test.result, res) + } + } +} + +func TestMergeWalks(t *testing.T) { + before := []pathWalk{ + {[][]string{{"path", "to", "file"}}, -1}, + // Foo/bar/baz cases cover that the outer and inner loops of the walk + // elimination need to be travel in opposite directions. + {[][]string{{"foo", "bar", "*.txt"}}, 2}, + {[][]string{{"foo", "baz", "*"}}, 2}, + // Ensure coverage of element removal from both end and middle. + {[][]string{{"path", "to", "other", "file"}}, -1}, + {[][]string{{"foo", "*"}}, 1}, + } + expected := []pathWalk{ + {[][]string{{"path", "to", "file"}}, -1}, + { + [][]string{ + {"foo", "bar", "*.txt"}, + {"foo", "baz", "*"}, + {"foo", "*"}, + }, 1, + }, + {[][]string{{"path", "to", "other", "file"}}, -1}, + } + after := mergeWalks(before) + + if !reflect.DeepEqual(after, expected) { + t.Fatalf("mergeWalks: \nexp'd %v \nactual %v", expected, after) + } +} + +func TestMapFiles(t *testing.T) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("error creating temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + wf1 := path.Join(tempDir, "globme.file") + f, err := os.Create(wf1) + if err != nil { + t.Fatalf("error creating test file: %v", err) + } + f.Close() + f, err = os.Create(path.Join(tempDir, "notme.file")) + if err != nil { + t.Fatalf("error creating test file: %v", err) + } + f.Close() + wd := path.Join(tempDir, "globdir") + if err := os.Mkdir(wd, 0755); err != nil { + t.Fatalf("error creating test directory: %v", err) + } + wf2 := path.Join(wd, "globmetoo.file") + f, err = os.Create(wf2) + if err != nil { + t.Fatalf("error creating test file: %v", err) + } + f.Close() + f, err = os.Create(path.Join(tempDir, "notmeeither.file")) + if err != nil { + t.Fatalf("error creating test file: %v", err) + } + f.Close() + + ps := []goolib.PkgSources{ + { + Include: []string{"**"}, + Exclude: []string{"notme*"}, + Target: "foo", + Root: tempDir, + }, + } + fm, err := mapFiles(ps) + if err != nil { + t.Fatalf("error getting file map: %v", err) + } + em := fileMap{"foo": []string{wf1}, "foo/globdir": []string{wf2}} + if !reflect.DeepEqual(fm, em) { + t.Errorf("did not get expected package map: got %v, want %v", fm, em) + } +} + +func TestWriteFiles(t *testing.T) { + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Errorf("error creating temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + wf := path.Join(tempDir, "test.pkg") + f, err := os.Create(wf) + if err != nil { + t.Errorf("error creating test package: %v", err) + } + f.Close() + fm := fileMap{"foo": []string{wf}} + ef := path.Join("foo", path.Base(wf)) + + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + if err := writeFiles(tw, fm); err != nil { + t.Errorf("error writing files to zip: %v", err) + } + if err := tw.Close(); err != nil { + t.Errorf("error closing zip writer: %v", err) + } + tr := tar.NewReader(buf) + hdr, err := tr.Next() + if err != nil { + t.Error(err) + } + if hdr.Name != ef { + t.Errorf("zip contains unexpected file: expect %q got %q", ef, f.Name()) + } +} diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..24c5c14 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,12 @@ +$googet_root = "$env:ProgramData\GooGet" + +$root = [Environment]::GetEnvironmentVariable('GooGetRoot', 'Machine') +if ($root -ne "$googet_root") { + [Environment]::SetEnvironmentVariable('GooGetRoot', "$googet_root", 'Machine') +} + +$path = [Environment]::GetEnvironmentVariable('Path', 'Machine') +if ($path -notlike "*%GooGetRoot%*") { + $path = $path + ";%GooGetRoot%" + [Environment]::SetEnvironmentVariable('Path', $path, 'Machine') +} diff --git a/install/install.go b/install/install.go new file mode 100644 index 0000000..b0b160a --- /dev/null +++ b/install/install.go @@ -0,0 +1,488 @@ +/* +Copyright 2016 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 install handles the installation of packages. +package install + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/google/googet/client" + "github.com/google/googet/download" + "github.com/google/googet/goolib" + "github.com/google/googet/system" + "github.com/google/logger" +) + +// minInstalled reports whether the package is installed at the given version or greater. +func minInstalled(pi goolib.PackageInfo, state client.GooGetState) (bool, error) { + for _, p := range state { + if p.PackageSpec.Name == pi.Name && (pi.Arch == "" || p.PackageSpec.Arch == pi.Arch) { + c, err := goolib.Compare(pi.Ver, p.PackageSpec.Version) + if err != nil { + return false, err + } + return c < 1, nil + } + } + return false, nil +} + +func installDeps(ps *goolib.PkgSpec, cache string, rm client.RepoMap, archs []string, state *client.GooGetState, dbOnly bool) error { + logger.Infof("Resolving dependencies for %s %s version %s", ps.Arch, ps.Name, ps.Version) + for p, ver := range ps.PkgDependencies { + pi := goolib.PkgNameSplit(p) + mi, err := minInstalled(goolib.PackageInfo{pi.Name, pi.Arch, ver}, *state) + if err != nil { + return err + } + if mi { + logger.Infof("Dependency met: %s.%s with version greater than %s installed", pi.Name, pi.Arch, ver) + continue + } + var ins bool + v, repo, arch, err := client.FindRepoLatest(goolib.PackageInfo{pi.Name, pi.Arch, ""}, rm, archs) + if err != nil { + return err + } + c, err := goolib.Compare(v, ver) + if err != nil { + return err + } + if c > -1 { + logger.Infof("Dependency found: %s.%s %s is available", pi.Name, arch, v) + if err := FromRepo(goolib.PackageInfo{pi.Name, arch, v}, repo, cache, rm, archs, state, dbOnly); err != nil { + return err + } + ins = true + } + if !ins { + return fmt.Errorf("cannot resolve dependancy, %s.%s version %s or greater not installed and not available in any repo", pi.Name, arch, ver) + } + } + return nil +} + +// Latest installs the latest version of a package. +func Latest(pi goolib.PackageInfo, cache string, rm client.RepoMap, archs []string, state *client.GooGetState, dbOnly bool) error { + ver, repo, arch, err := client.FindRepoLatest(pi, rm, archs) + if err != nil { + return err + } + return FromRepo(goolib.PackageInfo{pi.Name, arch, ver}, repo, cache, rm, archs, state, dbOnly) +} + +// FromRepo installs a package and all dependencies from a repository. +func FromRepo(pi goolib.PackageInfo, repo, cache string, rm client.RepoMap, archs []string, state *client.GooGetState, dbOnly bool) error { + ni, err := NeedsInstallation(pi, *state) + if err != nil { + return err + } + if !ni { + return nil + } + + logger.Infof("Starting install of %s.%s.%s", pi.Name, pi.Arch, pi.Ver) + fmt.Printf("Installing %s.%s.%s and dependencies...\n", pi.Name, pi.Arch, pi.Ver) + rs, err := client.FindRepoSpec(pi, rm[repo]) + if err != nil { + return err + } + if err := installDeps(rs.PackageSpec, cache, rm, archs, state, dbOnly); err != nil { + return err + } + + dst, err := download.FromRepo(rs, repo, cache) + if err != nil { + return err + } + + dir, err := extractPkg(dst) + if err != nil { + return err + } + + insFiles, err := installPkg(dir, rs.PackageSpec, dbOnly) + if err != nil { + return err + } + + logger.Infof("Installation of %s.%s.%s completed", pi.Name, pi.Arch, pi.Ver) + fmt.Printf("Installation of %s.%s.%s and all dependencies completed\n", pi.Name, pi.Arch, pi.Ver) + // Clean up old version, if applicable. + pi = goolib.PackageInfo{pi.Name, pi.Arch, ""} + if st, err := state.GetPackageState(pi); err == nil { + if !dbOnly { + cleanOldFiles(dir, st, insFiles) + } + if err := os.RemoveAll(st.UnpackDir); err != nil { + logger.Error(err) + } + if err := state.Remove(pi); err != nil { + return err + } + } + state.Add(client.PackageState{ + SourceRepo: repo, + DownloadURL: strings.TrimSuffix(repo, filepath.Base(repo)) + rs.Source, + Checksum: rs.Checksum, + UnpackDir: dir, + PackageSpec: rs.PackageSpec, + InstalledFiles: insFiles, + }) + return nil +} + +// FromDisk installs a local .goo file. +func FromDisk(arg, cache string, state *client.GooGetState, dbOnly, ri bool) error { + if _, err := os.Stat(arg); err != nil { + return err + } + + zs, err := extractSpec(arg) + if err != nil { + return fmt.Errorf("error extracting spec file: %v", err) + } + + if !ri { + ni, err := NeedsInstallation(goolib.PackageInfo{zs.Name, zs.Arch, zs.Version}, *state) + if err != nil { + return err + } + if !ni { + fmt.Printf("%s.%s.%s or a newer version is already installed on the system\n", zs.Name, zs.Arch, zs.Version) + return nil + } + } + + logger.Infof("Starting install of %q, version %q from %q", zs.Name, zs.Version, arg) + fmt.Printf("Installing %s %s...\n", zs.Name, zs.Version) + + for p, ver := range zs.PkgDependencies { + pi := goolib.PkgNameSplit(p) + mi, err := minInstalled(goolib.PackageInfo{pi.Name, pi.Arch, ver}, *state) + if err != nil { + return err + } + if mi { + logger.Infof("Dependency met: %s.%s with version greater than %s installed", pi.Name, pi.Arch, ver) + continue + } + return fmt.Errorf("Package dependency %s %s (min version %s) not installed.\n", pi.Name, pi.Arch, ver) + } + + dst := filepath.Join(cache, goolib.PackageInfo{zs.Name, zs.Arch, zs.Version}.PkgName()) + if err := copyPkg(arg, dst); err != nil { + return err + } + + dir, err := extractPkg(dst) + if err != nil { + return err + } + + insFiles, err := installPkg(dir, zs, dbOnly) + if err != nil { + return err + } + + if ri { + logger.Infof("Reinstallation of %q, version %q completed", zs.Name, zs.Version) + fmt.Printf("Reinstallation of %s completed\n", zs.Name) + return nil + } + + logger.Infof("Installation of %q, version %q completed", zs.Name, zs.Version) + fmt.Printf("Installation of %s completed\n", zs.Name) + + // Clean up old version, if applicable. + pi := goolib.PackageInfo{zs.Name, zs.Arch, ""} + if st, err := state.GetPackageState(pi); err == nil { + if !dbOnly { + cleanOldFiles(dir, st, insFiles) + } + if err := os.RemoveAll(st.UnpackDir); err != nil { + logger.Error(err) + } + if err := state.Remove(pi); err != nil { + return err + } + } + state.Add(client.PackageState{ + UnpackDir: dir, + PackageSpec: zs, + InstalledFiles: insFiles, + }) + return nil +} + +// Reinstall reinstalls and optionally redownloads, a package. +func Reinstall(ps client.PackageState, state client.GooGetState, rd bool) error { + pi := goolib.PackageInfo{ps.PackageSpec.Name, ps.PackageSpec.Arch, ps.PackageSpec.Version} + logger.Infof("Starting reinstall of %s.%s, version %s", pi.Name, pi.Arch, pi.Ver) + fmt.Printf("Reinstalling %s.%s %s and dependencies...\n", pi.Name, pi.Arch, pi.Ver) + _, err := os.Stat(ps.UnpackDir) + if err != nil && !os.IsNotExist(err) { + return err + } + if os.IsNotExist(err) { + logger.Infof("Package directory does not exist for %s.%s.%s, redownloading...", pi.Name, pi.Arch, pi.Ver) + rd = true + } + dir := ps.UnpackDir + if rd { + if ps.DownloadURL == "" { + return fmt.Errorf("can not redownload %s.%s.%s, DownloadURL not saved", pi.Name, pi.Arch, pi.Ver) + } + dst := ps.UnpackDir + ".goo" + if err := download.Package(ps.DownloadURL, dst, ps.Checksum); err != nil { + return fmt.Errorf("error redownloading package: %v", err) + } + dir, err = extractPkg(dst) + if err != nil { + return err + } + } + if _, err := installPkg(dir, ps.PackageSpec, false); err != nil { + return fmt.Errorf("error reinstalling package: %v", err) + } + + logger.Infof("Reinstallation of %s.%s, version %s completed", pi.Name, pi.Arch, pi.Ver) + fmt.Printf("Reinstallation of %s.%s %s completed\n", pi.Name, pi.Arch, pi.Ver) + return nil +} + +func copyPkg(src, dst string) (retErr error) { + r, err := os.Open(src) + if err != nil { + return err + } + defer r.Close() + + f, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if err := f.Close(); err != nil && retErr == nil { + retErr = err + } + }() + + if _, err := io.Copy(f, r); err != nil { + return err + } + return retErr +} + +func extractPkg(pkg string) (string, error) { + dir, err := download.ExtractPkg(pkg) + if err != nil { + return "", err + } + if err := os.Remove(pkg); err != nil { + logger.Errorf("error cleaning up package file: %v", err) + } + return dir, nil +} + +// NeedsInstallation checks if a package version needs installation. +func NeedsInstallation(pi goolib.PackageInfo, state client.GooGetState) (bool, error) { + for _, p := range state { + if p.PackageSpec.Name == pi.Name { + if p.PackageSpec.Arch != pi.Arch { + continue + } + c, err := goolib.Compare(p.PackageSpec.Version, pi.Ver) + if err != nil { + return true, err + } + switch c { + case 0: + logger.Infof("%s.%s %s is already installed.\n", pi.Name, pi.Arch, pi.Ver) + return false, nil + case 1: + logger.Infof("A newer version of %s.%s is already installed.\n", pi.Name, pi.Arch) + return false, nil + } + } + } + return true, nil +} + +func extractSpec(pkgPath string) (*goolib.PkgSpec, error) { + f, err := os.Open(pkgPath) + if err != nil { + return nil, err + } + defer f.Close() + + return goolib.ExtractPkgSpec(f) +} + +func makeInstallFunction(src, dst string, insFiles map[string]string, dbOnly bool) func(string, os.FileInfo, error) error { + return func(path string, fi os.FileInfo, err error) (outerr error) { + if err != nil { + return err + } + outPath := filepath.Join(dst, strings.TrimPrefix(path, src)) + if dbOnly { + if !fi.IsDir() { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + insFiles[outPath] = goolib.Checksum(f) + } + insFiles[outPath] = "" + return nil + } + if fi.IsDir() { + logger.Infof("Creating folder %q", outPath) + // We designate directories by an empty hash. + insFiles[outPath] = "" + return os.MkdirAll(outPath, fi.Mode()) + } + if err = client.RemoveOrRename(outPath); err != nil { + return err + } + logger.Infof("Copying file %q", outPath) + oFile, err := os.Create(outPath) + if err != nil { + if !os.IsNotExist(err) { + return err + } + if err := os.MkdirAll(filepath.Dir(outPath), fi.Mode()); err != nil { + return err + } + if oFile, err = os.Create(outPath); err != nil { + return err + } + } + defer func() { + if err := oFile.Close(); err != nil && outerr == nil { + outerr = err + } + }() + iFile, err := os.Open(path) + if err != nil { + return err + } + defer iFile.Close() + + hash := sha256.New() + mw := io.MultiWriter(oFile, hash) + if _, err := io.Copy(mw, iFile); err != nil { + return err + } + // TODO(ajackura): actually use file hash for verification and upgrade. + insFiles[outPath] = hex.EncodeToString(hash.Sum(nil)) + return nil + } +} + +func resolveDst(dst string) string { + if !filepath.IsAbs(dst) { + if strings.HasPrefix(dst, "<") { + if i := strings.LastIndex(dst, ">"); i != -1 { + return os.Getenv(dst[1:i]) + dst[i+1:] + } + } + return "/" + dst + } + return dst +} + +func cleanOldFiles(dir string, oldState client.PackageState, insFiles map[string]string) { + if len(oldState.InstalledFiles) == 0 { + return + } + var dirs []string + for file := range oldState.InstalledFiles { + if chksum, ok := insFiles[file]; !ok { + if chksum == "" { + dirs = append(dirs, file) + continue + } + logger.Infof("Cleaning up old file %q", file) + if err := client.RemoveOrRename(file); err != nil { + logger.Error(err) + } + } + } + sort.Sort(sort.Reverse(sort.StringSlice(dirs))) + for _, dir := range dirs { + if err := client.RemoveOrRename(dir); err != nil { + logger.Info(err) + } + } +} + +func installPkg(dir string, ps *goolib.PkgSpec, dbOnly bool) (map[string]string, error) { + logger.Infof("Executing install of package %q", filepath.Base(dir)) + insFiles := make(map[string]string) + for src, dst := range ps.Files { + dst = resolveDst(dst) + src = filepath.Join(dir, src) + if err := filepath.Walk(src, makeInstallFunction(src, dst, insFiles, dbOnly)); err != nil { + return nil, err + } + } + if dbOnly { + return insFiles, nil + } + return insFiles, system.Install(dir, ps) +} + +func listDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, dl []goolib.PackageInfo, archs []string) ([]goolib.PackageInfo, error) { + rs, err := client.FindRepoSpec(pi, rm[repo]) + if err != nil { + return nil, err + } + dl = append(dl, pi) + for d, v := range rs.PackageSpec.PkgDependencies { + di := goolib.PkgNameSplit(d) + ver, repo, arch, err := client.FindRepoLatest(di, rm, archs) + di.Arch = arch + if err != nil { + return nil, fmt.Errorf("cannot resolve dependency %s.%s.%s: %v", di.Name, di.Arch, di.Ver, err) + } + c, err := goolib.Compare(ver, v) + if err != nil { + return nil, err + } + if c == -1 { + return nil, fmt.Errorf("cannot resolve dependency, %s.%s version %s or greater not installed and not available in any repo", pi.Name, pi.Arch, pi.Ver) + } + di.Ver = ver + dl, err = listDeps(di, rm, repo, dl, archs) + if err != nil { + return nil, err + } + } + return dl, nil +} + +// ListDeps returns a list of dependencies and subdependancies for a package. +func ListDeps(pi goolib.PackageInfo, rm client.RepoMap, repo string, archs []string) ([]goolib.PackageInfo, error) { + logger.Infof("Building dependency list for %s.%s.%s", pi.Name, pi.Arch, pi.Ver) + return listDeps(pi, rm, repo, nil, archs) +} diff --git a/install/install_test.go b/install/install_test.go new file mode 100644 index 0000000..c4eef5a --- /dev/null +++ b/install/install_test.go @@ -0,0 +1,232 @@ +/* +Copyright 2016 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 install + +import ( + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/logger" +) + +func init() { + logger.Init("test", true, false, ioutil.Discard) +} + +func TestMinInstalled(t *testing.T) { + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "foo_pkg", + Version: "1.2.3@4", + Arch: "noarch", + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "bar_pkg", + Version: "0.1.0@1", + Arch: "noarch", + }, + }, + } + + table := []struct { + pkg, arch string + ins bool + }{ + {"foo_pkg", "noarch", true}, + {"foo_pkg", "", true}, + {"foo_pkg", "x86_64", false}, + {"bar_pkg", "noarch", false}, + {"baz_pkg", "noarch", false}, + } + for _, tt := range table { + ma, err := minInstalled(goolib.PackageInfo{tt.pkg, tt.arch, "1.0.0@1"}, state) + if err != nil { + t.Fatalf("error checking minAvailable: %v", err) + } + if ma != tt.ins { + t.Errorf("minInstalled returned %v for %q when it should return %v", ma, tt.pkg, tt.ins) + } + } +} + +func TestNeedsInstallation(t *testing.T) { + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: "foo_pkg", + Version: "1.0.0@1", + Arch: "noarch", + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "bar_pkg", + Version: "1.0.0@1", + Arch: "noarch", + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: "baz_pkg", + Version: "1.0.0@1", + Arch: "noarch", + }, + }, + } + + table := []struct { + pkg string + ver string + ins bool + }{ + {"foo_pkg", "1.0.0@1", false}, // equal + {"bar_pkg", "2.0.0@1", true}, // higher + {"baz_pkg", "0.1.0@1", false}, // lower + {"pkg", "1.0.0@1", true}, // not installed + } + for _, tt := range table { + ins, err := NeedsInstallation(goolib.PackageInfo{tt.pkg, "noarch", tt.ver}, state) + if err != nil { + t.Fatalf("Error checking NeedsInstallation: %v", err) + } + if ins != tt.ins { + t.Errorf("NeedsInstallation returned %v for %q when it should return %v", ins, tt.pkg, tt.ins) + } + } +} + +func TestInstallPkg(t *testing.T) { + src, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(src) + + dst, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(dst) + + files := []string{"test1", "test2", "test3"} + want := map[string]string{dst: ""} + for _, n := range files { + f, err := os.Create(filepath.Join(src, n)) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + want[filepath.Join(dst, n)] = goolib.Checksum(f) + if err := f.Close(); err != nil { + t.Fatalf("Failed to close test file: %v", err) + } + } + + ps := goolib.PkgSpec{Files: map[string]string{filepath.Base(src): dst}} + + got, err := installPkg(filepath.Dir(src), &ps, false) + if err != nil { + t.Fatalf("Error running installPkg: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Fatalf("installPkg did not return expected file list, got: %+v, want: %+v", got, want) + } + + for _, n := range files { + want := filepath.Join(dst, n) + if _, err := os.Stat(want); err != nil { + t.Errorf("Expected test file %s does not exist", want) + } + } +} + +func TestCleanOldFiles(t *testing.T) { + src, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(src) + + dst, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(dst) + + for _, n := range []string{filepath.Join(src, "test1"), filepath.Join(src, "test2")} { + if err := ioutil.WriteFile(n, []byte{}, 0666); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + } + + want := filepath.Join(dst, "test1") + notWant := filepath.Join(dst, "test2") + dontCare := filepath.Join(dst, "test3") + for _, n := range []string{want, notWant, dontCare} { + if err := ioutil.WriteFile(n, []byte{}, 0666); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + } + + st := client.PackageState{ + PackageSpec: &goolib.PkgSpec{ + Files: map[string]string{filepath.Base(src): dst}, + }, + InstalledFiles: map[string]string{ + want: "chksum", + notWant: "chksum", + dst: "", + }, + } + + cleanOldFiles(dst, st, map[string]string{want: "", dst: ""}) + + for _, n := range []string{want, dontCare} { + if _, err := os.Stat(n); err != nil { + t.Errorf("Expected test file %s does not exist", want) + } + } + + if _, err := os.Stat(notWant); err == nil { + t.Errorf("Deprecated file %s not removed", notWant) + } +} + +func TestResolveDst(t *testing.T) { + if err := os.Setenv("foo", "bar"); err != nil { + t.Errorf("error setting environment variable: %v", err) + } + + table := []struct { + dst, want string + }{ + {"/some/place", "bar/some/place"}, + {"/some/place", "/something//some/place"}, + } + for _, tt := range table { + got := resolveDst(tt.dst) + if got != tt.want { + t.Errorf("resolveDst returned %s, want %s", got, tt.want) + } + } +} diff --git a/remove/remove.go b/remove/remove.go new file mode 100644 index 0000000..ce5ca4a --- /dev/null +++ b/remove/remove.go @@ -0,0 +1,181 @@ +/* +Copyright 2016 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 remove handles the removal of packages. +package remove + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/google/googet/client" + "github.com/google/googet/download" + "github.com/google/googet/goolib" + "github.com/google/googet/system" + "github.com/google/logger" +) + +func uninstallPkg(pi goolib.PackageInfo, state *client.GooGetState, dbOnly bool) error { + logger.Infof("Executing removal of package %q", pi.Name) + ps, err := state.GetPackageState(pi) + if err != nil { + return fmt.Errorf("package not found in state file: %v", err) + } + if !dbOnly { + _, err := os.Stat(ps.UnpackDir) + if err != nil && !os.IsNotExist(err) { + return err + } + if os.IsNotExist(err) { + dst := ps.UnpackDir + ".goo" + logger.Infof("Package directory does not exist for %s.%s.%s, redownloading...", ps.PackageSpec.Name, ps.PackageSpec.Arch, ps.PackageSpec.Version) + if err := download.Package(ps.DownloadURL, dst, ps.Checksum); err != nil { + return fmt.Errorf("error redownloading %s.%s.%s, package may no longer exist in the repo, you can use the '-db_only' flag to remove it form the database: %v", pi.Name, pi.Arch, pi.Ver, err) + } + if _, err := download.ExtractPkg(dst); err != nil { + return err + } + if err := os.Remove(dst); err != nil { + logger.Errorf("error cleaning up package file: %v", err) + } + } + if err := system.Uninstall(ps); err != nil { + return err + } + if len(ps.InstalledFiles) > 0 { + var dirs []string + for file, chksum := range ps.InstalledFiles { + if chksum == "" { + dirs = append(dirs, file) + continue + } + logger.Infof("Removing %q", file) + if err := client.RemoveOrRename(file); err != nil { + logger.Error(err) + } + } + sort.Sort(sort.Reverse(sort.StringSlice(dirs))) + for _, dir := range dirs { + logger.Infof("Removing %q", dir) + if err := client.RemoveOrRename(dir); err != nil { + logger.Info(err) + } + } + } else { + // TODO(ajackura): Remove this 'else' once most packages have updated their + // state to include InstalledFiles. + for src, dst := range ps.PackageSpec.Files { + if !filepath.IsAbs(dst) { + if strings.HasPrefix(dst, "<") { + if i := strings.LastIndex(dst, ">"); i != -1 { + dst = os.Getenv(dst[1:i]) + dst[i+1:] + } else { + dst = "/" + dst + } + } else { + dst = "/" + dst + } + } + var toRemove []string + src = filepath.Join(ps.UnpackDir, src) + err = filepath.Walk(src, func(path string, fi os.FileInfo, err error) error { + if err == nil { + toRemove = append([]string{path}, toRemove...) + } + return nil + }) + for _, path := range toRemove { + outPath := filepath.Join(dst, strings.TrimPrefix(path, src)) + logger.Infof("Removing %q", outPath) + client.RemoveOrRename(outPath) + } + } + } + } + + if err := os.RemoveAll(ps.UnpackDir); err != nil { + logger.Errorf("error removing package data from cache directory: %v", err) + } + return state.Remove(pi) +} + +// DepMap is a map of packages to dependant packages. +type DepMap map[string][]string + +func (deps DepMap) remove(name string) { + for dep, s := range deps { + for i, d := range s { + if d == name { + s[i] = s[len(s)-1] + s = s[:len(s)-1] + deps[dep] = s + break + } + } + } + delete(deps, name) +} + +func (deps DepMap) build(name, arch string, state client.GooGetState) { + logger.Infof("Building dependency map for %q", name) + deps[name+"."+arch] = nil + for _, p := range state { + if p.PackageSpec.Name == name && p.PackageSpec.Arch == arch { + continue + } + for d := range p.PackageSpec.PkgDependencies { + di := goolib.PkgNameSplit(d) + if di.Name == name && (di.Arch == arch || di.Arch == "") { + n, a := p.PackageSpec.Name, p.PackageSpec.Arch + deps[name+"."+arch] = append(deps[name+"."+arch], n+"."+a) + deps.build(n, a, state) + } + } + } +} + +// EnumerateDeps returns a DepMap and list of dependencies for a package. +func EnumerateDeps(pi goolib.PackageInfo, state client.GooGetState) (DepMap, []string) { + dm := make(DepMap) + dm.build(pi.Name, pi.Arch, state) + var dl []string + for k := range dm { + di := goolib.PkgNameSplit(k) + ps, err := state.GetPackageState(di) + if err != nil { + logger.Fatalf("error finding package in state file, even though the dependancy map was just built: %v", err) + } + dl = append(dl, k+" "+ps.PackageSpec.Version) + } + return dm, dl +} + +// All removes a package and all dependant packages. Packages with no dependant packages +// will be removed first. +func All(pi goolib.PackageInfo, deps DepMap, state *client.GooGetState, dbOnly bool) error { + for len(deps) > 1 { + for dep := range deps { + if len(deps[dep]) == 0 { + di := goolib.PkgNameSplit(dep) + if err := uninstallPkg(di, state, dbOnly); err != nil { + return err + } + deps.remove(dep) + } + } + } + return uninstallPkg(pi, state, dbOnly) +} diff --git a/remove/remove_test.go b/remove/remove_test.go new file mode 100644 index 0000000..7786bb9 --- /dev/null +++ b/remove/remove_test.go @@ -0,0 +1,148 @@ +/* +Copyright 2016 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 remove + +import ( + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/logger" +) + +func init() { + logger.Init("test", true, false, ioutil.Discard) +} + +func TestUninstallPkg(t *testing.T) { + src, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(src) + + dst, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(dst) + + testFolder := filepath.Join(dst, "and") + testFolder2 := filepath.Join(testFolder, "another") + testFolder3 := filepath.Join(testFolder2, "level") + if err := os.MkdirAll(testFolder3, 0755); err != nil { + t.Fatalf("Failed to create test folder: %v", err) + } + + testFile := filepath.Join(testFolder3, "foo") + if err := ioutil.WriteFile(testFile, []byte{}, 0666); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + st := &client.GooGetState{ + client.PackageState{ + PackageSpec: &goolib.PkgSpec{ + Name: "foo", + }, + InstalledFiles: map[string]string{ + testFile: "chksum", + testFolder: "", + testFolder2: "", + testFolder3: "", + dst: "", + }, + UnpackDir: dst, + }, + } + + if err := uninstallPkg(goolib.PackageInfo{Name: "foo"}, st, false); err != nil { + t.Fatalf("Error running uninstallPkg: %v", err) + } + + for _, n := range []string{testFile, dst} { + if _, err := os.Stat(n); err == nil { + t.Errorf("%s was not removed", n) + } + } +} + +func TestBuild(t *testing.T) { + pkg1 := "foo_pkg" + pkg2 := "bar_pkg" + pkg3 := "baz_pkg" + as := ".noarch" + state := []client.PackageState{ + { + PackageSpec: &goolib.PkgSpec{ + Name: pkg1, + Version: "1.0.0@1", + Arch: "noarch", + PkgDependencies: map[string]string{ + pkg3 + as: "1.0.0@1", + }, + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: pkg2, + Version: "1.0.0@1", + Arch: "noarch", + PkgDependencies: map[string]string{ + pkg3 + as: "1.0.0@1", + pkg1 + as: "1.0.0@1", + }, + }, + }, + { + PackageSpec: &goolib.PkgSpec{ + Name: pkg3, + Version: "1.0.0@1", + Arch: "noarch", + }, + }, + } + + table := []struct { + pkg string + want DepMap + }{ + {pkg1, DepMap{pkg1 + as: []string{pkg2 + as}, pkg2 + as: nil}}, + {pkg2, DepMap{pkg2 + as: nil}}, + {pkg3, DepMap{pkg1 + as: []string{pkg2 + as}, pkg2 + as: nil, pkg3 + as: []string{pkg1 + as, pkg2 + as}}}, + } + for _, tt := range table { + deps := make(DepMap) + deps.build(tt.pkg, "noarch", state) + if !reflect.DeepEqual(deps, tt.want) { + t.Errorf("returned dependancy map does not match expected one: got %v, want %v", deps, tt.want) + } + } +} + +func TestRemoveDep(t *testing.T) { + pkg1 := "foo_pkg" + pkg2 := "bar_pkg" + pkg3 := "baz_pkg" + deps := DepMap{pkg1: []string{pkg2}, pkg2: nil, pkg3: []string{pkg1, pkg2}} + want := DepMap{pkg1: []string{}, pkg3: []string{pkg1}} + deps.remove(pkg2) + + if !reflect.DeepEqual(deps, want) { + t.Errorf("returned dependancy map does not match expected one: got %v, want %v", deps, want) + } +} diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..51d8e04 --- /dev/null +++ b/server/README.md @@ -0,0 +1,11 @@ +# GooGet Server + +This is a simple example of what a GooGet server looks like. +The server looks for a folder in it's root directory called 'packages', +creating it if necesary. The directory contents are read on a set +interval and all .goo packages served in the repo (localhost:8000/index by +default). + +Improvements to this design would include only updating the repository on +a package change as well as providing and api for adding/removing packages. + diff --git a/server/gooserve.go b/server/gooserve.go new file mode 100644 index 0000000..8277e5d --- /dev/null +++ b/server/gooserve.go @@ -0,0 +1,159 @@ +/* +Copyright 2016 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. +*/ + +// The gooserve binary is used to serve GooGet repositories. +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/google/googet/goolib" + "github.com/google/logger" +) + +var ( + root = flag.String("root", "", "root location") + interval = flag.Duration("interval", 5*time.Minute, "duration between refresh runs") + verbose = flag.Bool("verbose", false, "print info level logs to stdout") + systemLog = flag.Bool("system_log", false, "log to Linux Syslog or Windows Event Log") + port = flag.Int("port", 8000, "listen port") + + repoContents *repoPackages +) + +// repoPackages describes a repository of packages. +type repoPackages struct { + rs []goolib.RepoSpec + mu sync.Mutex +} + +// add provides a thread safe way to add a package to repoPackages. +func (r *repoPackages) add(src, chksum string, spec *goolib.PkgSpec) { + r.mu.Lock() + defer r.mu.Unlock() + r.rs = append(r.rs, goolib.RepoSpec{ + Source: src, + Checksum: chksum, + PackageSpec: spec, + }) +} + +func packageInfo(pkgPath, packageDir string) error { + pkg := filepath.Base(pkgPath) + pi := goolib.PkgNameSplit(strings.TrimSuffix(pkg, ".goo")) + + spec, err := extractSpec(pkgPath) + if err != nil { + return err + } + if spec.Name != pi.Name { + return fmt.Errorf("%s: name in spec does not match package file name", pkgPath) + } + if spec.Arch != pi.Arch { + return fmt.Errorf("%s: arch in spec does not match package file name", pkgPath) + } + if spec.Version != pi.Ver { + return fmt.Errorf("%s: version in spec does not match package version", pkgPath) + } + + f, err := os.Open(pkgPath) + if err != nil { + return err + } + defer f.Close() + + repoContents.add(filepath.Join(packageDir, pkg), goolib.Checksum(f), spec) + return nil +} + +func runSync(packageDir string) error { + logger.Info("Beginning sync run") + if err := os.MkdirAll(packageDir, 0774); err != nil { + return err + } + + pkgs, err := filepath.Glob(filepath.Join(packageDir, "*.goo")) + if err != nil { + return err + } + + // TODO(ajackura): Don't reset repoContents each run, just append + // or remove as needed. + repoContents = &repoPackages{} + var wg sync.WaitGroup + for _, pkg := range pkgs { + wg.Add(1) + go func(pkg string) { + defer wg.Done() + if err := packageInfo(pkg, packageDir); err != nil { + logger.Error(err) + } + }(pkg) + } + wg.Wait() + logger.Info("Sync run completed successfully") + return nil +} + +// extractSpec takes a goopkg file and returns the unmarshalled spec file. +func extractSpec(pkgPath string) (*goolib.PkgSpec, error) { + f, err := os.Open(pkgPath) + if err != nil { + return nil, err + } + defer f.Close() + return goolib.ExtractPkgSpec(f) +} + +func serve(w http.ResponseWriter, r *http.Request) { + out, err := json.MarshalIndent(repoContents.rs, "", " ") + if err != nil { + logger.Fatal(err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(out) +} + +func main() { + flag.Parse() + + logger.Init("GooServe", *verbose, *systemLog, ioutil.Discard) + + http.HandleFunc("/index", serve) + go func() { + err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil) + if err != nil { + logger.Fatal(err) + } + }() + + packageDir := filepath.Join(*root, "packages") + if err := runSync(packageDir); err != nil { + logger.Error(err) + } + + for range time.Tick(*interval) { + if err := runSync(packageDir); err != nil { + logger.Error(err) + } + } +} diff --git a/system/system_linux.go b/system/system_linux.go new file mode 100644 index 0000000..2a7a8be --- /dev/null +++ b/system/system_linux.go @@ -0,0 +1,79 @@ +/* +Copyright 2016 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. +*/ + +// +build linux + +// Package system handles system specific functions. +package system + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/logger" +) + +// Install performs a system specfic install given a package extraction directory and an PkgSpec struct. +func Install(dir string, ps *goolib.PkgSpec) error { + in := ps.Install + if in.Path == "" { + logger.Info("No installer specified") + return nil + } + + logger.Infof("Running install: %q", in.Path) + out, err := os.Create(filepath.Join(dir, "googet_install.log")) + if err != nil { + return err + } + defer func() { + if err := out.Close(); err != nil { + logger.Error(err) + } + }() + if err := goolib.Exec(filepath.Join(dir, in.Path), in.Args, in.ExitCodes, out); err != nil { + return fmt.Errorf("error running install: %v", err) + } + return nil +} + +// Uninstall performs a system specfic uninstall given a packages PackageState. +func Uninstall(st client.PackageState) error { + un := st.PackageSpec.Uninstall + if un.Path == "" { + logger.Info("No uninstaller specified") + return nil + } + + logger.Infof("Running uninstall: %q", un.Path) + // logging is only useful for failed uninstalls + out, err := os.Create(filepath.Join(st.UnpackDir, "googet_remove.log")) + if err != nil { + return err + } + defer func() { + if err := out.Close(); err != nil { + logger.Error(err) + } + }() + return goolib.Exec(filepath.Join(st.UnpackDir, un.Path), un.Args, un.ExitCodes, out) +} + +// InstallableArchs returns a slice of archs supported by this machine. +func InstallableArchs() ([]string, error) { + // Just return all archs as Linux builds are currently just used for testing. + return []string{"noarch", "x86_64", "x86_32", "arm"}, nil +} diff --git a/system/system_windows.go b/system/system_windows.go new file mode 100644 index 0000000..ee804af --- /dev/null +++ b/system/system_windows.go @@ -0,0 +1,196 @@ +/* +Copyright 2016 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. +*/ + +// +build windows + +// Package system handles system specific functions. +package system + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/StackExchange/wmi" + "github.com/google/googet/client" + "github.com/google/googet/goolib" + "github.com/google/logger" + "golang.org/x/sys/windows/registry" +) + +const regBase = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + +var msiSuccessCodes = []int{1641, 3010} + +func addUninstallEntry(dir string, ps *goolib.PkgSpec) error { + reg := regBase + "GooGet - " + ps.Name + logger.Infof("Adding uninstall entry %q to registry.", reg) + k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, reg, registry.WRITE) + if err != nil { + return err + } + defer k.Close() + + exe := filepath.Join(os.Getenv("GooGetRoot"), "googet.exe") + + table := []struct { + name, value string + }{ + {"UninstallString", fmt.Sprintf("%s -no_confirm remove %s", exe, ps.Name)}, + {"InstallLocation", dir}, + {"DisplayVersion", ps.Version}, + {"DisplayName", "GooGet - " + ps.Name}, + } + for _, re := range table { + if err := k.SetStringValue(re.name, re.value); err != nil { + return err + } + } + return nil +} + +func removeUninstallEntry(name string) error { + reg := regBase + "GooGet - " + name + logger.Infof("Removing uninstall entry %q from registry.", reg) + return registry.DeleteKey(registry.LOCAL_MACHINE, reg) +} + +// Install performs a system specfic install given a package extraction directory and a PkgSpec struct. +func Install(dir string, ps *goolib.PkgSpec) error { + in := ps.Install + if in.Path == "" { + logger.Info("No installer specified") + return nil + } + + logger.Infof("Running install: %q", in.Path) + out, err := os.Create(filepath.Join(dir, in.Path+".log")) + if err != nil { + return err + } + defer func() { + if err := out.Close(); err != nil { + logger.Error(err) + } + }() + s := filepath.Join(dir, in.Path) + msiLog := filepath.Join(dir, "msi_install.log") + switch filepath.Ext(s) { + case ".msi": + args := append([]string{"/i", s, "/qn", "/norestart", "/log", msiLog}, in.Args...) + ec := append(msiSuccessCodes, in.ExitCodes...) + err = goolib.Run(exec.Command("msiexec", args...), ec, out) + case ".msp": + args := append([]string{"/update", s, "/qn", "/norestart", "/log", msiLog}, in.Args...) + ec := append(msiSuccessCodes, in.ExitCodes...) + err = goolib.Run(exec.Command("msiexec", args...), ec, out) + case ".msu": + args := append([]string{s, "/quiet", "/norestart"}, in.Args...) + err = goolib.Run(exec.Command("wusa", args...), in.ExitCodes, out) + case ".exe": + err = goolib.Run(exec.Command(s, in.Args...), in.ExitCodes, out) + default: + err = goolib.Exec(s, in.Args, in.ExitCodes, out) + } + if err != nil { + return err + } + + if err := addUninstallEntry(dir, ps); err != nil { + logger.Error(err) + } + return nil +} + +// Uninstall performs a system specfic uninstall given a packages PackageState. +func Uninstall(st client.PackageState) error { + un := st.PackageSpec.Uninstall + if un.Path == "" { + logger.Info("No uninstaller specified") + return nil + } + + logger.Infof("Running uninstall: %q", un.Path) + // logging is only useful for failed uninstall + out, err := os.Create(filepath.Join(st.UnpackDir, un.Path+".log")) + if err != nil { + return err + } + defer func() { + if err := out.Close(); err != nil { + logger.Error(err) + } + }() + s := filepath.Join(st.UnpackDir, un.Path) + switch filepath.Ext(s) { + case ".msi": + msiLog := filepath.Join(st.UnpackDir, "msi_uninstall.log") + args := append([]string{"/x", s, "/qn", "/norestart", "/log", msiLog}, un.Args...) + ec := append(msiSuccessCodes, un.ExitCodes...) + err = goolib.Run(exec.Command("msiexec", args...), ec, out) + case ".msu": + args := append([]string{s, "/uninstall", "/quiet", "/norestart"}, un.Args...) + err = goolib.Run(exec.Command("wusa", args...), un.ExitCodes, out) + case ".exe": + err = goolib.Run(exec.Command(s, un.Args...), un.ExitCodes, out) + default: + err = goolib.Exec(filepath.Join(st.UnpackDir, un.Path), un.Args, un.ExitCodes, out) + } + if err != nil { + return err + } + + if err := removeUninstallEntry(st.PackageSpec.Name); err != nil { + logger.Error(err) + } + return nil +} + +type win32_OperatingSystem struct { + AddressWidth uint16 +} + +func width() (int, error) { + var os []win32_OperatingSystem + if err := wmi.Query(wmi.CreateQuery(&os, ""), &os); err != nil { + return 0, err + } + return int(os[0].AddressWidth), nil +} + +// InstallableArchs returns a slice of archs supported by this machine. +// WMI errors are logged but not returned. +func InstallableArchs() ([]string, error) { + switch { + case runtime.GOARCH == "386": + // Check if this is indeed a 32bit system. + aw, err := width() + if err != nil { + logger.Errorf("Error getting AddressWidth: %v", err) + return []string{"noarch", "x86_32"}, nil + } + if int(aw) == 32 { + return []string{"noarch", "x86_32"}, nil + } + return []string{"noarch", "x86_64", "x86_32"}, nil + case runtime.GOARCH == "amd64": + // TODO: Add check for 32bit support, make sure it works with servers and client OS's. + return []string{"noarch", "x86_32", "x86_64"}, nil + case runtime.GOARCH == "arm": + return []string{"noarch", "arm"}, nil + default: + return nil, fmt.Errorf("runtime %s not supported", runtime.GOARCH) + } +}