Commit 796a7e1d authored by Jochen Sprickerhof's avatar Jochen Sprickerhof

Update upstream source from tag 'upstream/0.5.0'

Update to upstream version '0.5.0'
with Debian dir c600289a7c0ada3f320d63af50a9bc14270cabc3
parents d4a6f2f6 392bf229
......@@ -5,3 +5,6 @@ cmd/fdroidcl/fdroidcl
*.xml
*.jar
*-etag
# Allow testdata/staticrepo
!/testdata/staticrepo/*
language: go
go:
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12beta2
go_import_path: mvdan.cc/fdroidcl
env:
- GO111MODULE=on
install: true
script:
- go test ./...
......@@ -3,9 +3,9 @@
[![GoDoc](https://godoc.org/github.com/mvdan/fdroidcl?status.svg)](https://godoc.org/mvdan.cc/fdroidcl)
[![Build Status](https://travis-ci.org/mvdan/fdroidcl.svg?branch=master)](https://travis-ci.org/mvdan/fdroidcl)
[F-Droid](https://f-droid.org/) desktop client.
[F-Droid](https://f-droid.org/) desktop client. Requires Go 1.11 or later.
go get -u mvdan.cc/fdroidcl/cmd/fdroidcl
go get -u mvdan.cc/fdroidcl
While the Android client integrates with the system with regular update checks
and notifications, this is a simple command line client that talks to connected
......@@ -28,16 +28,24 @@ Install an app:
### Commands
update Update the index
search <regexp...> Search available apps
search [<regexp...>] Search available apps
show <appid...> Show detailed info about an app
devices List connected devices
download <appid...> Download an app
install <appid...> Install or upgrade app
install [<appid...>] Install or upgrade apps
uninstall <appid...> Uninstall an app
download <appid...> Download an app
devices List connected devices
list (categories) List all known values of a kind
defaults Reset to the default settings
version Print version information
An appid is just an app's unique package name. A specific version of an app can
be selected by following the appid with a colon and the version code. The
'search' and 'show' commands can be used to find these strings. For example:
A specific version of an app can be selected by following the appid with an
colon (:) and the version code of the app to select.
$ fdroidcl search redreader
$ fdroidcl show org.quantumbadger.redreader
$ fdroidcl install org.quantumbadger.redreader:85
### Config
......
......@@ -9,14 +9,11 @@ import (
"path/filepath"
)
// Cache returns the base cache directory.
func Cache() string {
return cache()
}
// TODO: replace with https://github.com/golang/go/issues/29960 if accepted.
// Data returns the base data directory.
func Data() string {
return data()
return dataDir
}
func firstGetenv(def string, evs ...string) string {
......@@ -25,17 +22,14 @@ func firstGetenv(def string, evs ...string) string {
return v
}
}
home, err := homeDir()
if err != nil {
return ""
// TODO: replace with os.UserHomeDir once we require Go 1.12 or later.
home := os.Getenv("HOME")
if home == "" {
curUser, err := user.Current()
if err != nil {
return ""
}
home = curUser.HomeDir
}
return filepath.Join(home, def)
}
func homeDir() (string, error) {
curUser, err := user.Current()
if err != nil {
return "", err
}
return curUser.HomeDir, nil
}
......@@ -3,15 +3,4 @@
package basedir
var (
cacheDir = firstGetenv("Library/Caches")
dataDir = firstGetenv("Library/Application Support")
)
func cache() string {
return cacheDir
}
func data() string {
return dataDir
}
var dataDir = firstGetenv("Library/Application Support")
......@@ -5,15 +5,4 @@
package basedir
var (
cacheDir = firstGetenv(".cache", "XDG_CACHE_HOME")
dataDir = firstGetenv(".config", "XDG_CONFIG_HOME")
)
func cache() string {
return cacheDir
}
func data() string {
return dataDir
}
var dataDir = firstGetenv(".config", "XDG_CONFIG_HOME")
......@@ -3,15 +3,4 @@
package basedir
var (
cacheDir = firstGetenv("", "TEMP", "TMP")
dataDir = firstGetenv("", "APPDATA")
)
func cache() string {
return cacheDir
}
func data() string {
return dataDir
}
var dataDir = firstGetenv("", "APPDATA")
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package main
import (
"bytes"
"io/ioutil"
"net/http"
"os"
"regexp"
"testing"
"time"
"mvdan.cc/fdroidcl/adb"
)
// chosenApp is the app that will be installed and uninstalled on a connected
// device. This one was chosen because it's tiny, requires no permissions, and
// should be compatible with every device.
//
// It also stores no data, so it is fine to uninstall it and the user won't lose
// any data.
const chosenApp = "org.vi_server.red_screen"
func TestCommands(t *testing.T) {
url := config.Repos[0].URL
client := http.Client{Timeout: 2 * time.Second}
if _, err := client.Get(url); err != nil {
t.Skipf("skipping since %s is unreachable: %v", url, err)
}
dir, err := ioutil.TempDir("", "fdroidcl")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
testBasedir = dir
mustSucceed := func(t *testing.T, wantRe, negRe string, cmd *Command, args ...string) {
mustRun(t, true, wantRe, negRe, cmd, args...)
}
mustFail := func(t *testing.T, wantRe, negRe string, cmd *Command, args ...string) {
mustRun(t, false, wantRe, negRe, cmd, args...)
}
t.Run("Version", func(t *testing.T) {
mustSucceed(t, `^v`, ``, cmdVersion)
})
t.Run("SearchBeforeUpdate", func(t *testing.T) {
mustFail(t, `could not open index`, ``, cmdSearch)
})
t.Run("UpdateFirst", func(t *testing.T) {
mustSucceed(t, `done`, ``, cmdUpdate)
})
t.Run("UpdateCached", func(t *testing.T) {
mustSucceed(t, `not modified`, ``, cmdUpdate)
})
t.Run("SearchNoArgs", func(t *testing.T) {
mustSucceed(t, `F-Droid`, ``, cmdSearch)
})
t.Run("SearchWithArgs", func(t *testing.T) {
mustSucceed(t, `F-Droid`, ``, cmdSearch, "fdroid.fdroid")
})
t.Run("SearchWithArgsNone", func(t *testing.T) {
mustSucceed(t, `^$`, ``, cmdSearch, "nomatches")
})
t.Run("SearchOnlyPackageNames", func(t *testing.T) {
mustSucceed(t, `^[^ ]*$`, ``, cmdSearch, "-q", "fdroid.fdroid")
})
t.Run("ShowOne", func(t *testing.T) {
mustSucceed(t, `fdroid/fdroidclient`, ``, cmdShow, "org.fdroid.fdroid")
})
t.Run("ShowMany", func(t *testing.T) {
mustSucceed(t, `fdroid/fdroidclient.*fdroid/privileged-extension`, ``,
cmdShow, "org.fdroid.fdroid", "org.fdroid.fdroid.privileged")
})
t.Run("ListCategories", func(t *testing.T) {
mustSucceed(t, `Development`, ``, cmdList, "categories")
})
if err := startAdbIfNeeded(); err != nil {
t.Log("skipping the device tests as ADB is not installed")
return
}
devices, err := adb.Devices()
if err != nil {
t.Fatal(err)
}
switch len(devices) {
case 0:
t.Log("skipping the device tests as none was found via ADB")
return
case 1:
// continue below
default:
t.Log("skipping the device tests as too many were found via ADB")
return
}
t.Run("DevicesOne", func(t *testing.T) {
mustSucceed(t, `\n`, ``, cmdDevices)
})
// try to uninstall the app first
devices[0].Uninstall(chosenApp)
t.Run("UninstallMissing", func(t *testing.T) {
mustFail(t, `not installed$`, ``, cmdUninstall, chosenApp)
})
t.Run("SearchInstalledMissing", func(t *testing.T) {
mustSucceed(t, ``, regexp.QuoteMeta(chosenApp), cmdSearch, "-i", "-q")
})
t.Run("SearchUpgradableMissing", func(t *testing.T) {
mustSucceed(t, ``, regexp.QuoteMeta(chosenApp), cmdSearch, "-u", "-q")
})
t.Run("InstallVersioned", func(t *testing.T) {
mustSucceed(t, `Installing `+regexp.QuoteMeta(chosenApp), ``,
cmdInstall, chosenApp+":1")
})
t.Run("SearchInstalled", func(t *testing.T) {
time.Sleep(3 * time.Second)
mustSucceed(t, regexp.QuoteMeta(chosenApp), ``, cmdSearch, "-i", "-q")
})
t.Run("SearchUpgradable", func(t *testing.T) {
mustSucceed(t, regexp.QuoteMeta(chosenApp), ``, cmdSearch, "-u", "-q")
})
t.Run("InstallUpgrade", func(t *testing.T) {
mustSucceed(t, `Installing `+regexp.QuoteMeta(chosenApp), ``,
cmdInstall, chosenApp)
})
t.Run("SearchUpgradableUpToDate", func(t *testing.T) {
mustSucceed(t, ``, regexp.QuoteMeta(chosenApp), cmdSearch, "-u", "-q")
})
t.Run("InstallUpToDate", func(t *testing.T) {
mustSucceed(t, `is up to date$`, ``, cmdInstall, chosenApp)
})
t.Run("UninstallExisting", func(t *testing.T) {
mustSucceed(t, `Uninstalling `+regexp.QuoteMeta(chosenApp), ``,
cmdUninstall, chosenApp)
})
}
func mustRun(t *testing.T, success bool, wantRe, negRe string, cmd *Command, args ...string) {
var buf bytes.Buffer
stdout, stderr = &buf, &buf
err := cmd.Run(args)
out := buf.String()
if err != nil {
out += err.Error()
}
if success && err != nil {
t.Fatalf("unexpected error: %v\n%s", err, out)
} else if !success && err == nil {
t.Fatalf("expected error, got none\n%s", out)
}
// Let '.' match newlines, and treat the output as a single line.
wantRe = "(?sm)" + wantRe
if !regexp.MustCompile(wantRe).MatchString(out) {
t.Fatalf("output does not match %#q:\n%s", wantRe, out)
}
if negRe != "" {
negRe = "(?sm)" + negRe
if regexp.MustCompile(negRe).MatchString(out) {
t.Fatalf("output does match %#q:\n%s", negRe, out)
}
}
}
......@@ -27,7 +27,7 @@ func runDevices(args []string) error {
return fmt.Errorf("could not get devices: %v", err)
}
for _, device := range devices {
fmt.Fprintf(stdout, "%s - %s (%s)\n", device.ID, device.Model, device.Product)
fmt.Printf("%s - %s (%s)\n", device.ID, device.Model, device.Product)
}
return nil
}
......
......@@ -7,7 +7,7 @@ import (
"fmt"
"path/filepath"
"mvdan.cc/fdroidcl"
"mvdan.cc/fdroidcl/fdroid"
)
var cmdDownload = &Command{
......@@ -27,10 +27,8 @@ func runDownload(args []string) error {
if err != nil {
return err
}
device, err := maybeOneDevice()
if err != nil {
return err
}
// don't fail a download if adb is not installed
device, _ := maybeOneDevice()
for _, app := range apps {
apk := app.SuggestedApk(device)
if apk == nil {
......@@ -40,12 +38,12 @@ func runDownload(args []string) error {
if err != nil {
return err
}
fmt.Fprintf(stdout, "APK available in %s\n", path)
fmt.Printf("APK available in %s\n", path)
}
return nil
}
func downloadApk(apk *fdroidcl.Apk) (string, error) {
func downloadApk(apk *fdroid.Apk) (string, error) {
url := apk.URL()
path := apkPath(apk.ApkName)
if err := downloadEtag(url, path, apk.Hash); err == errNotModified {
......
// Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package fdroidcl
package fdroid
import (
"encoding/hex"
......
// Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package fdroidcl
package fdroid
import (
"encoding/json"
"encoding/xml"
"fmt"
"html"
"io"
"sort"
"strings"
......@@ -272,6 +273,10 @@ func LoadIndexJSON(r io.Reader) (*Index, error) {
if !enOK {
english, enOK = app.Localized["en-US"]
}
// TODO: why does the json index contain html escapes?
app.Name = html.UnescapeString(app.Name)
if app.Summary == "" && enOK {
app.Summary = english.Summary
}
......@@ -284,6 +289,10 @@ func LoadIndexJSON(r io.Reader) (*Index, error) {
apk := &index.Packages[app.PackageName][i]
apk.AppID = app.PackageName
apk.RepoURL = index.Repo.Address
// TODO: why does the json index contain html escapes?
apk.VersName = html.UnescapeString(apk.VersName)
app.Apks = append(app.Apks, apk)
}
}
......
// Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package fdroidcl
package fdroid
import (
"bytes"
......
// Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package fdroidcl
package fdroid
import (
"archive/zip"
......
module mvdan.cc/fdroidcl
require (
github.com/kr/pretty v0.1.0
github.com/rogpeppe/go-internal v1.1.0
)
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/rogpeppe/go-internal v1.1.0 h1:g0fH8RicVgNl+zVZDCDfbdWxAWoAEJyI7I3TZYXFiig=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
......@@ -4,38 +4,89 @@
package main
import (
"encoding/csv"
"fmt"
"io"
"os"
"mvdan.cc/fdroidcl"
"mvdan.cc/fdroidcl/adb"
"mvdan.cc/fdroidcl/fdroid"
)
var cmdInstall = &Command{
UsageLine: "install <appid...>",
Short: "Install or upgrade an app",
UsageLine: "install [<appid...>]",
Short: "Install or upgrade apps",
Long: `
Install or upgrade apps. When given no arguments, it reads a comma-separated
list of apps to install from standard input, like:
packageName,versionCode,versionName
foo.bar,120,1.2.0
`[1:],
}
var (
installUpdates = cmdInstall.Fset.Bool("u", false, "Upgrade all installed apps")
installDryRun = cmdInstall.Fset.Bool("n", false, "Only print the operations that would be done")
)
func init() {
cmdInstall.Run = runInstall
}
func runInstall(args []string) error {
if len(args) < 1 {
return fmt.Errorf("no package names given")
if *installUpdates && len(args) > 0 {
return fmt.Errorf("-u can only be used without arguments")
}
device, err := oneDevice()
if err != nil {
return err
}
apps, err := findApps(args)
inst, err := device.Installed()
if err != nil {
return err
}
inst, err := device.Installed()
if *installUpdates {
apps, err := loadIndexes()
if err != nil {
return err
}
apps = filterAppsUpdates(apps, inst, device)
if len(apps) == 0 {
fmt.Fprintln(os.Stderr, "All apps up to date.")
}
return downloadAndDo(apps, device)
}
if len(args) == 0 {
// The CSV input is as follows:
//
// packageName,versionCode,versionName
// foo.bar,120,1.2.0
// ...
r := csv.NewReader(os.Stdin)
r.FieldsPerRecord = 3
r.Read()
for {
record, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("error parsing CSV: %v", err)
}
// convert "foo.bar,120" into "foo.bar:120" for findApps
args = append(args, record[0]+":"+record[1])
}
}
apps, err := findApps(args)
if err != nil {
return err
}
var toInstall []*fdroidcl.App
var toInstall []fdroid.App
for _, app := range apps {
p, e := inst[app.PackageName]
if !e {
......@@ -48,7 +99,7 @@ func runInstall(args []string) error {
return fmt.Errorf("no suitable APKs found for %s", app.PackageName)
}
if p.VersCode >= suggested.VersCode {
fmt.Fprintf(stdout, "%s is up to date\n", app.PackageName)
fmt.Printf("%s is up to date\n", app.PackageName)
// app is already up to date
continue
}
......@@ -58,9 +109,9 @@ func runInstall(args []string) error {
return downloadAndDo(toInstall, device)
}
func downloadAndDo(apps []*fdroidcl.App, device *adb.Device) error {
func downloadAndDo(apps []fdroid.App, device *adb.Device) error {
type downloaded struct {
apk *fdroidcl.Apk
apk *fdroid.Apk
path string
}
toInstall := make([]downloaded, len(apps))
......@@ -69,12 +120,19 @@ func downloadAndDo(apps []*fdroidcl.App, device *adb.Device) error {
if apk == nil {
return fmt.Errorf("no suitable APKs found for %s", app.PackageName)
}
if *installDryRun {
fmt.Printf("install %s:%d\n", app.PackageName, apk.VersCode)
continue
}
path, err := downloadApk(apk)
if err != nil {
return err
}
toInstall[i] = downloaded{apk: apk, path: path}
}
if *installDryRun {
return nil
}
for _, t := range toInstall {
if err := installApk(device, t.apk, t.path); err != nil {
return err
......@@ -83,8 +141,8 @@ func downloadAndDo(apps []*fdroidcl.App, device *adb.Device) error {
return nil
}
func installApk(device *adb.Device, apk *fdroidcl.Apk, path string) error {
fmt.Fprintf(stdout, "Installing %s\n", apk.AppID)
func installApk(device *adb.Device, apk *fdroid.Apk, path string) error {
fmt.Printf("Installing %s\n", apk.AppID)
if err := device.Install(path); err != nil {
return fmt.Errorf("could not install %s: %v", apk.AppID, err)
}
......
......@@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
"sort"
)
......@@ -42,7 +43,7 @@ func runList(args []string) error {
}
sort.Strings(result)
for _, s := range result {
fmt.Fprintln(stdout, s)
fmt.Fprintln(os.Stdout, s)
}
return nil
}
......@@ -7,7 +7,6 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
......@@ -17,46 +16,30 @@ import (
const cmdName = "fdroidcl"
const version = "v0.4.0"
func errExit(format string, a ...interface{}) {
fmt.Fprintf(stderr, format, a...)
os.Exit(1)
}
const version = "v0.5.0"
func subdir(dir, name string) string {
p := filepath.Join(dir, name)
if err := os.MkdirAll(p, 0755); err != nil {
errExit("Could not create dir '%s': %v\n", p, err)
fmt.Fprintf(os.Stderr, "Could not create dir '%s': %v\n", p, err)
}
return p
}
var (
stdout io.Writer = os.Stdout
stderr io.Writer = os.Stderr
testBasedir = ""
)
func mustCache() string {
if testBasedir != "" {
return subdir(testBasedir, "cache")
}
dir := basedir.Cache()
if dir == "" {
errExit("Could not determine cache dir\n")
dir, err := os.UserCacheDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
panic("TODO: return an error")
}
return subdir(dir, cmdName)
}
func mustData() string {
if testBasedir != "" {
return subdir(testBasedir, "data")
}
dir := basedir.Data()
if dir == "" {
errExit("Could not determine data dir\n")
fmt.Fprintln(os.Stderr, "Could not determine data dir")
panic("TODO: return an error")
}
return subdir(dir, cmdName)
}
......@@ -113,8 +96,13 @@ type Command struct {
// The first word in the line is taken to be the command name.
UsageLine string
// Short is the short description.
// Short is the short, single-line description.
Short string
// Long is an optional longer version of the Short description.
Long string
Fset flag.FlagSet
}
// Name returns the command's name: the first word in the usage line.
......@@ -127,21 +115,25 @@ func (c *Command) Name() string {
return name
}
func (c *Command) usage(flagSet *flag.FlagSet) {
fmt.Fprintf(stderr, "Usage: %s %s [-h]\n", cmdName, c.UsageLine)
func (c *Command) usage() {
fmt.Fprintf(os.Stderr, "usage: %s %s\n\n", cmdName, c.UsageLine)
if c.Long == "" {
fmt.Fprintf(os.Stderr, "%s.\n", c.Short)
} else {
fmt.Fprint(os.Stderr, c.Long)
}
anyFlags := false
flagSet.VisitAll(func(f *flag.Flag) { anyFlags = true })
c.Fset.VisitAll(func(f *flag.Flag) { anyFlags = true })
if anyFlags {
fmt.Fprintf(stderr, "\nAvailable options:\n")
flagSet.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nAvailable options:\n")
c.Fset.PrintDefaults()
}
os.Exit(2)
}
func init() {
flag.Usage = func() {
fmt.Fprintf(stderr, "Usage: %s [-h] <command> [<args>]\