all: minimal set of changes for iOS app (#9619)

### Purpose

This PR contains the set of changes needed to make Syncthing work on iOS
for [my iOS app for
Syncthing](https://github.com/pixelspark/sushitrain).

Most changes originate from [the Mobius Sync
fork](http://github.com/MobiusSync/syncthing/tree/ios). I have removed
the changes from their fork that are not strictly needed for my app
(i.e. their changes to the GUI and command line utilities, for instance)
and squashed it all in a single commit.

In summary, the changes are:

* Resolve non-absolute paths to the 'Documents' folder (basically the
only one an app can/should write user data to by default on iOS)
* Tweaking of build flags/conditions for iOS (i.e. determine which
basicfs_watch, ignoreresult variant to build for iOS)
* Disable upgrade mechanism on iOS
* Make `RequestGlobal` and `PullerProgress` public symbols
* Expose syncthing.app's Model instance (app.M)
* Add no-op stub for SetLowPriority on iOS

I would very much appreciate these changes to be (eventually) merged to
mainline syncthing, as this would allow my iOS app to track the mainline
source code directly and removes the need (for me at least) for
maintaining a separate fork. Perhaps the Mobius folks can also benefit
from this (although as noted this branch does not contain their changes
to e.g. the GUI).

### Testing

This branch has been tested with the iOS app and appears to work fine.
The full set of MobiusSync changes has been used before with success.

### Screenshots

n/a

### Documentation

There should be no visible changes for users due to this set of changes.

---------

Co-authored-by: Simon Pickup <simon@pickupinfinity.com>
This commit is contained in:
Tommy van der Vorst 2024-07-31 07:31:14 +02:00 committed by GitHub
parent e738af7c56
commit de0b4270df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 156 additions and 30 deletions

View File

@ -305,6 +305,7 @@ Severin von Wnuck-Lipinski <ss7@live.de>
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com> Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com>
Simon Frei (imsodin) <freisim93@gmail.com> Simon Frei (imsodin) <freisim93@gmail.com>
Simon Mwepu <simonmwepu@gmail.com> Simon Mwepu <simonmwepu@gmail.com>
Simon Pickup <simon@pickupinfinity.com>
Sly_tom_cat <slytomcat@mail.ru> Sly_tom_cat <slytomcat@mail.ru>
Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com> Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com>
Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org> <stefan@rumpelsepp.org> Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org> <stefan@rumpelsepp.org>
@ -325,6 +326,7 @@ Tobias Tom (tobiastom) <t.tom@succont.de>
Tom Jakubowski <tom@crystae.net> Tom Jakubowski <tom@crystae.net>
Tomasz Wilczyński <5626656+tomasz1986@users.noreply.github.com> <twilczynski@naver.com> Tomasz Wilczyński <5626656+tomasz1986@users.noreply.github.com> <twilczynski@naver.com>
Tommy Thorn <tommy-github-email@thorn.ws> Tommy Thorn <tommy-github-email@thorn.ws>
Tommy van der Vorst <tommy-github@pixelspark.nl>
Tully Robinson (tojrobinson) <tully@tojr.org> Tully Robinson (tojrobinson) <tully@tojr.org>
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com> Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
Tyler Kropp <kropptyler@gmail.com> Tyler Kropp <kropptyler@gmail.com>

View File

@ -4,10 +4,12 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/. // You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build !(solaris && !cgo) && !(darwin && !cgo) && !(android && amd64) //go:build !(solaris && !cgo) && !(darwin && !cgo) && !(darwin && kqueue) && !(android && amd64) && !ios
// +build !solaris cgo // +build !solaris cgo
// +build !darwin cgo // +build !darwin cgo
// +build !darwin !kqueue
// +build !android !amd64 // +build !android !amd64
// +build !ios
package fs package fs

View File

@ -4,8 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/. // You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build darwin && !kqueue && cgo //go:build darwin && !kqueue && cgo && !ios
// +build darwin,!kqueue,cgo // +build darwin,!kqueue,cgo,!ios
package fs package fs

View File

@ -4,8 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/. // You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build !linux && !windows && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !darwin && !cgo //go:build !linux && !windows && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !darwin && !cgo && !ios
// +build !linux,!windows,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!darwin,!cgo // +build !linux,!windows,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!darwin,!cgo,!ios
// Catch all platforms that are not specifically handled to use the generic // Catch all platforms that are not specifically handled to use the generic
// event types. // event types.

View File

@ -4,8 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/. // You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build (solaris && !cgo) || (darwin && !cgo) || (android && amd64) //go:build (solaris && !cgo) || (darwin && !cgo) || (android && amd64) || ios || (darwin && kqueue)
// +build solaris,!cgo darwin,!cgo android,amd64 // +build solaris,!cgo darwin,!cgo android,amd64 ios darwin,kqueue
package fs package fs

View File

@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build windows || darwin //go:build windows || darwin || ios
package ignoreresult package ignoreresult

View File

@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows && !darwin //go:build !windows && !darwin && !ios
package ignoreresult package ignoreresult

View File

@ -180,7 +180,7 @@ func defaultConfigDir(userHome string) string {
case build.IsWindows: case build.IsWindows:
return windowsConfigDataDir() return windowsConfigDataDir()
case build.IsDarwin: case build.IsDarwin, build.IsIOS:
return darwinConfigDataDir(userHome) return darwinConfigDataDir(userHome)
default: default:
@ -191,7 +191,7 @@ func defaultConfigDir(userHome string) string {
// defaultDataDir returns the default data directory, where we store the // defaultDataDir returns the default data directory, where we store the
// database, log files, etc. // database, log files, etc.
func defaultDataDir(userHome, configDir string) string { func defaultDataDir(userHome, configDir string) string {
if build.IsWindows || build.IsDarwin { if build.IsWindows || build.IsDarwin || build.IsIOS {
return configDir return configDir
} }

View File

@ -1579,7 +1579,7 @@ loop:
activity.using(selected) activity.using(selected)
var buf []byte var buf []byte
blockNo := int(state.block.Offset / int64(state.file.BlockSize())) blockNo := int(state.block.Offset / int64(state.file.BlockSize()))
buf, lastError = f.model.requestGlobal(f.ctx, selected.ID, f.folderID, state.file.Name, blockNo, state.block.Offset, int(state.block.Size), state.block.Hash, state.block.WeakHash, selected.FromTemporary) buf, lastError = f.model.RequestGlobal(f.ctx, selected.ID, f.folderID, state.file.Name, blockNo, state.block.Offset, int(state.block.Size), state.block.Hash, state.block.WeakHash, selected.FromTemporary)
activity.done(selected) activity.done(selected)
if lastError != nil { if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, selected.ID.Short(), "returned error:", lastError) l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, selected.ID.Short(), "returned error:", lastError)

View File

@ -261,7 +261,7 @@ func (c *folderSummaryService) processUpdate(ev events.Event) {
return return
case events.DownloadProgress: case events.DownloadProgress:
data := ev.Data.(map[string]map[string]*pullerProgress) data := ev.Data.(map[string]map[string]*PullerProgress)
c.foldersMut.Lock() c.foldersMut.Lock()
for folder := range data { for folder := range data {
c.folders[folder] = struct{}{} c.folders[folder] = struct{}{}

View File

@ -435,6 +435,28 @@ type Model struct {
result1 protocol.RequestResponse result1 protocol.RequestResponse
result2 error result2 error
} }
RequestGlobalStub func(context.Context, protocol.DeviceID, string, string, int, int64, int, []byte, uint32, bool) ([]byte, error)
requestGlobalMutex sync.RWMutex
requestGlobalArgsForCall []struct {
arg1 context.Context
arg2 protocol.DeviceID
arg3 string
arg4 string
arg5 int
arg6 int64
arg7 int
arg8 []byte
arg9 uint32
arg10 bool
}
requestGlobalReturns struct {
result1 []byte
result2 error
}
requestGlobalReturnsOnCall map[int]struct {
result1 []byte
result2 error
}
ResetFolderStub func(string) error ResetFolderStub func(string) error
resetFolderMutex sync.RWMutex resetFolderMutex sync.RWMutex
resetFolderArgsForCall []struct { resetFolderArgsForCall []struct {
@ -2558,6 +2580,84 @@ func (fake *Model) RequestReturnsOnCall(i int, result1 protocol.RequestResponse,
}{result1, result2} }{result1, result2}
} }
func (fake *Model) RequestGlobal(arg1 context.Context, arg2 protocol.DeviceID, arg3 string, arg4 string, arg5 int, arg6 int64, arg7 int, arg8 []byte, arg9 uint32, arg10 bool) ([]byte, error) {
var arg8Copy []byte
if arg8 != nil {
arg8Copy = make([]byte, len(arg8))
copy(arg8Copy, arg8)
}
fake.requestGlobalMutex.Lock()
ret, specificReturn := fake.requestGlobalReturnsOnCall[len(fake.requestGlobalArgsForCall)]
fake.requestGlobalArgsForCall = append(fake.requestGlobalArgsForCall, struct {
arg1 context.Context
arg2 protocol.DeviceID
arg3 string
arg4 string
arg5 int
arg6 int64
arg7 int
arg8 []byte
arg9 uint32
arg10 bool
}{arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8Copy, arg9, arg10})
stub := fake.RequestGlobalStub
fakeReturns := fake.requestGlobalReturns
fake.recordInvocation("RequestGlobal", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8Copy, arg9, arg10})
fake.requestGlobalMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *Model) RequestGlobalCallCount() int {
fake.requestGlobalMutex.RLock()
defer fake.requestGlobalMutex.RUnlock()
return len(fake.requestGlobalArgsForCall)
}
func (fake *Model) RequestGlobalCalls(stub func(context.Context, protocol.DeviceID, string, string, int, int64, int, []byte, uint32, bool) ([]byte, error)) {
fake.requestGlobalMutex.Lock()
defer fake.requestGlobalMutex.Unlock()
fake.RequestGlobalStub = stub
}
func (fake *Model) RequestGlobalArgsForCall(i int) (context.Context, protocol.DeviceID, string, string, int, int64, int, []byte, uint32, bool) {
fake.requestGlobalMutex.RLock()
defer fake.requestGlobalMutex.RUnlock()
argsForCall := fake.requestGlobalArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6, argsForCall.arg7, argsForCall.arg8, argsForCall.arg9, argsForCall.arg10
}
func (fake *Model) RequestGlobalReturns(result1 []byte, result2 error) {
fake.requestGlobalMutex.Lock()
defer fake.requestGlobalMutex.Unlock()
fake.RequestGlobalStub = nil
fake.requestGlobalReturns = struct {
result1 []byte
result2 error
}{result1, result2}
}
func (fake *Model) RequestGlobalReturnsOnCall(i int, result1 []byte, result2 error) {
fake.requestGlobalMutex.Lock()
defer fake.requestGlobalMutex.Unlock()
fake.RequestGlobalStub = nil
if fake.requestGlobalReturnsOnCall == nil {
fake.requestGlobalReturnsOnCall = make(map[int]struct {
result1 []byte
result2 error
})
}
fake.requestGlobalReturnsOnCall[i] = struct {
result1 []byte
result2 error
}{result1, result2}
}
func (fake *Model) ResetFolder(arg1 string) error { func (fake *Model) ResetFolder(arg1 string) error {
fake.resetFolderMutex.Lock() fake.resetFolderMutex.Lock()
ret, specificReturn := fake.resetFolderReturnsOnCall[len(fake.resetFolderArgsForCall)] ret, specificReturn := fake.resetFolderReturnsOnCall[len(fake.resetFolderArgsForCall)]
@ -3258,6 +3358,8 @@ func (fake *Model) Invocations() map[string][][]interface{} {
defer fake.remoteNeedFolderFilesMutex.RUnlock() defer fake.remoteNeedFolderFilesMutex.RUnlock()
fake.requestMutex.RLock() fake.requestMutex.RLock()
defer fake.requestMutex.RUnlock() defer fake.requestMutex.RUnlock()
fake.requestGlobalMutex.RLock()
defer fake.requestGlobalMutex.RUnlock()
fake.resetFolderMutex.RLock() fake.resetFolderMutex.RLock()
defer fake.resetFolderMutex.RUnlock() defer fake.resetFolderMutex.RUnlock()
fake.restoreFolderVersionsMutex.RLock() fake.restoreFolderVersionsMutex.RLock()

View File

@ -117,6 +117,8 @@ type Model interface {
DismissPendingFolder(device protocol.DeviceID, folder string) error DismissPendingFolder(device protocol.DeviceID, folder string) error
GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly bool) ([]*TreeEntry, error) GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly bool) ([]*TreeEntry, error)
RequestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error)
} }
type model struct { type model struct {
@ -375,7 +377,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
// it'll show up as errored later. // it'll show up as errored later.
if err := cfg.CreateRoot(); err != nil { if err := cfg.CreateRoot(); err != nil {
l.Warnln("Failed to create folder root directory", err) l.Warnln("Failed to create folder root directory:", err)
} else if err = cfg.CreateMarker(); err != nil { } else if err = cfg.CreateMarker(); err != nil {
l.Warnln("Failed to create folder marker:", err) l.Warnln("Failed to create folder marker:", err)
} }
@ -2460,7 +2462,7 @@ func (m *model) deviceDidCloseRLocked(deviceID protocol.DeviceID, duration time.
} }
} }
func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) { func (m *model) RequestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
conn, connOK := m.requestConnectionForDevice(deviceID) conn, connOK := m.requestConnectionForDevice(deviceID)
if !connOK { if !connOK {
return nil, fmt.Errorf("requestGlobal: no connection to device: %s", deviceID.Short()) return nil, fmt.Errorf("requestGlobal: no connection to device: %s", deviceID.Short())
@ -2566,7 +2568,7 @@ func (m *model) numHashers(folder string) int {
return folderCfg.Hashers return folderCfg.Hashers
} }
if build.IsWindows || build.IsDarwin || build.IsAndroid { if build.IsWindows || build.IsDarwin || build.IsIOS || build.IsAndroid {
// Interactive operating systems; don't load the system too heavily by // Interactive operating systems; don't load the system too heavily by
// default. // default.
return 1 return 1

View File

@ -222,7 +222,7 @@ func BenchmarkRequestOut(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
data, err := m.requestGlobal(context.Background(), device1, "default", files[i%n].Name, 0, 0, 32, nil, 0, false) data, err := m.RequestGlobal(context.Background(), device1, "default", files[i%n].Name, 0, 0, 32, nil, 0, false)
if err != nil { if err != nil {
b.Error(err) b.Error(err)
} }

View File

@ -118,12 +118,12 @@ func (t *ProgressEmitter) Serve(ctx context.Context) error {
} }
func (t *ProgressEmitter) sendDownloadProgressEventLocked() { func (t *ProgressEmitter) sendDownloadProgressEventLocked() {
output := make(map[string]map[string]*pullerProgress) output := make(map[string]map[string]*PullerProgress)
for folder, pullers := range t.registry { for folder, pullers := range t.registry {
if len(pullers) == 0 { if len(pullers) == 0 {
continue continue
} }
output[folder] = make(map[string]*pullerProgress) output[folder] = make(map[string]*PullerProgress)
for name, puller := range pullers { for name, puller := range pullers {
output[folder][name] = puller.Progress() output[folder][name] = puller.Progress()
} }

View File

@ -39,7 +39,7 @@ func expectEvent(w events.Subscription, t *testing.T, size int) {
if event.Type != events.DownloadProgress { if event.Type != events.DownloadProgress {
t.Fatal("Unexpected event:", event, "at", caller(1)) t.Fatal("Unexpected event:", event, "at", caller(1))
} }
data := event.Data.(map[string]map[string]*pullerProgress) data := event.Data.(map[string]map[string]*PullerProgress)
if len(data) != size { if len(data) != size {
t.Fatal("Unexpected event data size:", data, "at", caller(1)) t.Fatal("Unexpected event data size:", data, "at", caller(1))
} }

View File

@ -75,7 +75,7 @@ func newSharedPullerState(file protocol.FileInfo, fs fs.Filesystem, folderID, te
} }
// A momentary state representing the progress of the puller // A momentary state representing the progress of the puller
type pullerProgress struct { type PullerProgress struct {
Total int `json:"total"` Total int `json:"total"`
Reused int `json:"reused"` Reused int `json:"reused"`
CopiedFromOrigin int `json:"copiedFromOrigin"` CopiedFromOrigin int `json:"copiedFromOrigin"`
@ -405,13 +405,13 @@ func encryptionTrailerSize(file protocol.FileInfo) int64 {
} }
// Progress returns the momentarily progress for the puller // Progress returns the momentarily progress for the puller
func (s *sharedPullerState) Progress() *pullerProgress { func (s *sharedPullerState) Progress() *PullerProgress {
s.mut.RLock() s.mut.RLock()
defer s.mut.RUnlock() defer s.mut.RUnlock()
total := s.reused + s.copyTotal + s.pullTotal total := s.reused + s.copyTotal + s.pullTotal
done := total - s.copyNeeded - s.pullNeeded done := total - s.copyNeeded - s.pullNeeded
file := len(s.file.Blocks) file := len(s.file.Blocks)
return &pullerProgress{ return &PullerProgress{
Total: total, Total: total,
Reused: s.reused, Reused: s.reused,
CopiedFromOrigin: s.copyOrigin, CopiedFromOrigin: s.copyOrigin,

View File

@ -0,0 +1,16 @@
// Copyright (C) 2020 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build ios
// +build ios
package osutil
// SetLowPriority not possible on some platforms
// I/O priority depending on the platform and OS.
func SetLowPriority() error {
return nil
}

View File

@ -4,8 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build (!windows && !linux) || android //go:build (!windows && !linux && !ios) || android
// +build !windows,!linux android // +build !windows,!linux,!ios android
package osutil package osutil

View File

@ -553,7 +553,7 @@ func (w *walker) walkSymlink(ctx context.Context, relPath string, info fs.FileIn
// normalizePath returns the normalized relative path (possibly after fixing // normalizePath returns the normalized relative path (possibly after fixing
// it on disk), or skip is true. // it on disk), or skip is true.
func (w *walker) normalizePath(path string, info fs.FileInfo) (normPath string, err error) { func (w *walker) normalizePath(path string, info fs.FileInfo) (normPath string, err error) {
if build.IsDarwin { if build.IsDarwin || build.IsIOS {
// Mac OS X file names should always be NFD normalized. // Mac OS X file names should always be NFD normalized.
normPath = norm.NFD.String(path) normPath = norm.NFD.String(path)
} else { } else {

View File

@ -77,6 +77,7 @@ type App struct {
stopOnce sync.Once stopOnce sync.Once
mainServiceCancel context.CancelFunc mainServiceCancel context.CancelFunc
stopped chan struct{} stopped chan struct{}
Model model.Model
} }
func New(cfg config.Wrapper, dbBackend backend.Backend, evLogger events.Logger, cert tls.Certificate, opts Options) (*App, error) { func New(cfg config.Wrapper, dbBackend backend.Backend, evLogger events.Logger, cert tls.Certificate, opts Options) (*App, error) {
@ -249,6 +250,7 @@ func (a *App) startup() error {
keyGen := protocol.NewKeyGenerator() keyGen := protocol.NewKeyGenerator()
m := model.NewModel(a.cfg, a.myID, a.ll, protectedFiles, a.evLogger, keyGen) m := model.NewModel(a.cfg, a.myID, a.ll, protectedFiles, a.evLogger, keyGen)
a.Model = m
a.mainService.Add(m) a.mainService.Add(m)

View File

@ -4,8 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !noupgrade //go:build !noupgrade && !ios
// +build !noupgrade // +build !noupgrade,!ios
package upgrade package upgrade

View File

@ -4,8 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file, // License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/. // You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build noupgrade //go:build noupgrade || ios
// +build noupgrade // +build noupgrade ios
package upgrade package upgrade