Skip to content

Commit 409ec70

Browse files
committed
Update launch script and packaging
1 parent cae55df commit 409ec70

6 files changed

Lines changed: 188 additions & 46 deletions

File tree

Sources/FairCore/Info.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ let Info : [String : Any] = [
44
"CFBundleIdentifier" : "org.appfair.Fair",
55
"CFBundleInfoDictionaryVersion" : "6.0",
66
"CFBundlePackageType" : "FMWK",
7-
"CFBundleShortVersionString" : "0.9.4",
7+
"CFBundleShortVersionString" : "0.9.5",
88
"NSHumanReadableCopyright" : "GNU Affero General Public License",
99
]

Sources/FairCore/Resources/FairCore.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<key>CFBundlePackageType</key>
1212
<string>FMWK</string>
1313
<key>CFBundleShortVersionString</key>
14-
<string>0.9.4</string>
14+
<string>0.9.5</string>
1515
<key>NSHumanReadableCopyright</key>
1616
<string>GNU Affero General Public License</string>
1717
</dict>

Sources/fairtool/FairToolCommand.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public struct FairToolCommand : AsyncParsableCommand {
1515
public static var configuration = CommandConfiguration(
1616
commandName: "fairtool",
1717
abstract: "Manage an ecosystem of apps",
18+
version: Bundle.fairCoreVersion?.versionString ?? "unknown",
1819
shouldDisplay: !experimental,
1920
subcommands: [
2021
AppCommand.self,

Sources/fairtool/SourceCommand.swift

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public struct SourceCommand : AsyncParsableCommand {
6868
let appVersion = tokenParts.count > 1 ? tokenParts.dropFirst().first?.description : nil
6969

7070
msg(.info, "creating app item for: \(appName) version=\(appVersion ?? "")")
71-
let item = try await createAppItem(token: appName, version: appVersion)
71+
let item = try await createFDroidPackage(token: appName, version: appVersion)
7272
packageList.append(item)
7373
}
7474

@@ -85,13 +85,41 @@ public struct SourceCommand : AsyncParsableCommand {
8585
return catalog
8686
}
8787

88-
public func createAppItem(token: String, version latestVersion: String?) async throws -> (String, FDroidIndex.Package) {
88+
public func createFDroidPackage(token: String, version latestVersion: String?) async throws -> (String, FDroidIndex.Package) {
8989
msg(.info, "creating f-droid catalog for token: \(token)")
9090

9191
let version = try await fetchLatestVersion(token: token, unless: latestVersion)
9292
let dataSource = try await fetchSourceZip(token: token, version: version)
9393
let pathPrefix = (dataSource.paths.first?.pathName ?? "") + "/" // e.g.: "Tune-Out-1.0.2/"
94-
//let relativePaths = dataSource.paths.map(\.pathName).map({ $0.dropFirst(pathPrefix.count).description })
94+
let relativePaths = dataSource.paths.map(\.pathName).map({ $0.dropFirst(pathPrefix.count).description })
95+
96+
let fastlaneMetadataPrefix = "Android/fastlane/metadata/android"
97+
func loadAndroidFastlaneMetadata(_ path: String, locale: String) throws -> String? {
98+
String(data: try dataSource.data(atPath: pathPrefix + "\(fastlaneMetadataPrefix)/\(locale)/\(path)"), encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
99+
}
100+
101+
// build the list of locales from everything under the Android/fastlane/metadata/android/ path
102+
let locales = relativePaths
103+
.filter({ $0.hasPrefix(fastlaneMetadataPrefix + "/") })
104+
.map({ $0.split(separator: "/") })
105+
.filter({ $0.count == 5 })
106+
.compactMap({ $0.last?.description })
107+
108+
func loadFastlaneMetadata(_ key: String) -> FDroidIndex.LocalizedText? {
109+
var dict = FDroidIndex.LocalizedText()
110+
111+
for locale in locales {
112+
if let value = try? loadAndroidFastlaneMetadata(key, locale: locale) {
113+
dict[locale] = value
114+
}
115+
}
116+
117+
if !dict.isEmpty {
118+
return dict
119+
} else {
120+
return nil
121+
}
122+
}
95123

96124
let envFileData = try dataSource.data(atPath: pathPrefix + "Skip.env")
97125
let envFile = try EnvFile(data: envFileData)
@@ -115,7 +143,10 @@ public struct SourceCommand : AsyncParsableCommand {
115143
let manifest = FDroidIndex.Package.Manifest(versionName: "", versionCode: 0)
116144
let packageVersion = FDroidIndex.Package.PackageVersion(added: 0, file: file, manifest: manifest)
117145

118-
let metadata = FDroidIndex.Package.Metadata(added: 0, lastUpdated: 0)
146+
var metadata = FDroidIndex.Package.Metadata(added: 0, lastUpdated: 0)
147+
metadata.name = loadFastlaneMetadata("title.txt")
148+
metadata.summary = loadFastlaneMetadata("short_description.txt")
149+
metadata.description = loadFastlaneMetadata("full_description.txt")
119150
let package = FDroidIndex.Package(metadata: metadata, versions: [version: packageVersion])
120151

121152
return (appIdentifier, package)
@@ -175,7 +206,7 @@ public struct SourceCommand : AsyncParsableCommand {
175206
let appVersion = tokenParts.count > 1 ? tokenParts.dropFirst().first?.description : nil
176207

177208
msg(.info, "creating app item for: \(appName) version=\(appVersion ?? "")")
178-
let item = try await createAppItem(token: appName, version: appVersion)
209+
let item = try await createAltCatalogAppItem(token: appName, version: appVersion)
179210
apps.append(item)
180211
}
181212

@@ -200,15 +231,15 @@ public struct SourceCommand : AsyncParsableCommand {
200231
let json = try catalog.toJSON(outputFormatting: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes], dateEncodingStrategy: .iso8601, dataEncodingStrategy: .base64)
201232

202233
// upload the generated catalog to the GitHub releases
203-
_ = try await FileManager.default.withTemporaryFile(named: "altstore.json", contents: json) { path in
234+
_ = try await fm.withTemporaryFile(named: "altstore.json", contents: json) { path in
204235
try await githubReleaseUpload(appToken: sourceItem.appToken, version: appVersion, paths: [path])
205236
}
206237
}
207238

208239
return catalog
209240
}
210241

211-
func createAppItem(token appToken: String, version releaseVersion: String?) async throws -> (appToken: String, appItem: AltCatalogAppItem) {
242+
func createAltCatalogAppItem(token appToken: String, version releaseVersion: String?) async throws -> (appToken: String, appItem: AltCatalogAppItem) {
212243
let version = try await fetchLatestVersion(token: appToken, unless: releaseVersion)
213244
let dataSource = try await fetchSourceZip(token: appToken, version: version)
214245
let pathPrefix = (dataSource.paths.first?.pathName ?? "") + "/" // e.g.: "Tune-Out-1.0.2/"
@@ -266,18 +297,18 @@ public struct SourceCommand : AsyncParsableCommand {
266297
throw AppError("AltStoreEndpoint was invalid: \(sourceOptions.marketplaceService)")
267298
}
268299
let downloadFile = try await MarketplaceEndpoint(endpointBase: marketplaceEndpoint).download(adpid: adpid, logger: { msg(.info, $0) })
269-
defer { try? FileManager.default.removeItem(at: downloadFile) }
300+
defer { try? fm.removeItem(at: downloadFile) }
270301

271-
let tmpFolder = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
302+
let tmpFolder = fm.temporaryDirectory.appendingPathComponent(UUID().uuidString)
272303

273-
try FileManager.default.unzipItem(at: downloadFile, to: tmpFolder)
274-
defer { try? FileManager.default.removeItem(at: tmpFolder) }
304+
try fm.unzipItem(at: downloadFile, to: tmpFolder)
305+
defer { try? fm.removeItem(at: tmpFolder) }
275306

276307
// ensure that the manifest data exists in the folder
277308
let manifestData = try Data(contentsOf: tmpFolder.appendingPathComponent("manifest.json"))
278309
manifest = try JSONDecoder().decode(ADPManifest.self, from: manifestData)
279310

280-
let adpExtractedPaths = try FileManager.default.enumeratedURLs(of: tmpFolder)
311+
let adpExtractedPaths = try fm.enumeratedURLs(of: tmpFolder)
281312

282313
// now upload the contents of the zip to the GitHub repository
283314
let ghOut = try await githubReleaseUpload(appToken: appToken, version: marketingVersion, paths: adpExtractedPaths.filter(\.pathIsRegularFile))

Tests/FairToolTests/FairCommandTests.swift

Lines changed: 139 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -209,47 +209,156 @@ final class FairCommandTests: XCTestCase {
209209

210210
XCTAssertEqual(3, output.apps.count)
211211

212-
let firstApp = try XCTUnwrap(output.apps.dropFirst(0).first, "catalog should have contained at least one app")
213-
XCTAssertEqual("TuneOut", firstApp.name)
214-
XCTAssertEqual("org.appfair.app.Tune-Out", firstApp.bundleIdentifier)
215-
XCTAssertEqual("other", firstApp.category) // FIXME
216-
XCTAssertEqual("1639901758", firstApp.marketplaceID)
217-
XCTAssertEqual("Stream internet radio", firstApp.subtitle)
218-
XCTAssertEqual(1, firstApp.versions?.count)
219-
220-
let secondApp = try XCTUnwrap(output.apps.dropFirst(1).first, "catalog should have contained a second app")
221-
XCTAssertEqual("NetSkip", secondApp.name)
222-
XCTAssertEqual("org.appfair.app.Net-Skip", secondApp.bundleIdentifier)
223-
XCTAssertEqual("other", secondApp.category) // FIXME
224-
XCTAssertEqual("1640618584", secondApp.marketplaceID)
225-
XCTAssertEqual("A humane web browser", secondApp.subtitle)
226-
XCTAssertEqual(1, secondApp.versions?.count)
227-
XCTAssertEqual(netSkipVersion, secondApp.versions?.first?.version)
228-
229-
let thirdApp = try XCTUnwrap(output.apps.dropFirst(2).first, "catalog should have contained a third app")
230-
XCTAssertEqual("SkipNotes", thirdApp.name)
231-
XCTAssertEqual("org.appfair.app.SkipNotes", thirdApp.bundleIdentifier)
232-
XCTAssertEqual("other", thirdApp.category) // FIXME
233-
XCTAssertEqual("6740916318", thirdApp.marketplaceID)
234-
XCTAssertEqual("Simple and secure notes", thirdApp.subtitle)
235-
XCTAssertEqual(1, thirdApp.versions?.count)
236-
}
237-
238-
func testSourceCreateFDrdoidCommand() async throws {
212+
let tuneOut = try XCTUnwrap(output.apps.dropFirst(0).first, "catalog should have contained at least one app")
213+
XCTAssertEqual("TuneOut", tuneOut.name)
214+
XCTAssertEqual("org.appfair.app.Tune-Out", tuneOut.bundleIdentifier)
215+
XCTAssertEqual("other", tuneOut.category) // FIXME
216+
XCTAssertEqual("1639901758", tuneOut.marketplaceID)
217+
XCTAssertEqual("Stream internet radio", tuneOut.subtitle)
218+
XCTAssertEqual(1, tuneOut.versions?.count)
219+
220+
let netSkip = try XCTUnwrap(output.apps.dropFirst(1).first, "catalog should have contained a second app")
221+
XCTAssertEqual("NetSkip", netSkip.name)
222+
XCTAssertEqual("org.appfair.app.Net-Skip", netSkip.bundleIdentifier)
223+
XCTAssertEqual("other", netSkip.category) // FIXME
224+
XCTAssertEqual("1640618584", netSkip.marketplaceID)
225+
XCTAssertEqual("A humane web browser", netSkip.subtitle)
226+
XCTAssertEqual(1, netSkip.versions?.count)
227+
XCTAssertEqual(netSkipVersion, netSkip.versions?.first?.version)
228+
229+
let skipNotes = try XCTUnwrap(output.apps.dropFirst(2).first, "catalog should have contained a third app")
230+
XCTAssertEqual("SkipNotes", skipNotes.name)
231+
XCTAssertEqual("org.appfair.app.SkipNotes", skipNotes.bundleIdentifier)
232+
XCTAssertEqual("other", skipNotes.category) // FIXME
233+
XCTAssertEqual("6740916318", skipNotes.marketplaceID)
234+
XCTAssertEqual("Simple and secure notes", skipNotes.subtitle)
235+
XCTAssertEqual(1, skipNotes.versions?.count)
236+
}
237+
238+
func testSourceCreateFDroidCommand() async throws {
239239
let netSkipVersion = "1.4.5"
240240
let args = ["Tune-Out", "Net-Skip/\(netSkipVersion)", "Skip-Notes"]
241241

242242
let (output, messages) = try await runToolOutputSingle(SourceCommand.configuration, SourceCommand.CreateCommand.configuration, cmd: SourceCommand.CreateFDroidCatalogCommand.self, args: Array(args))
243243

244244
_ = messages
245245

246-
//let output = result.output.joined()
247-
dbg("output:", output)
248-
249246
XCTAssertEqual(3, output.packages?.count)
250247

248+
let tuneOut = try XCTUnwrap(output.packages?["org.appfair.app.Tune_Out"], "missing app")
249+
XCTAssertEqual("TuneOut", tuneOut.metadata.name?["en-US"])
250+
251+
let netSkip = try XCTUnwrap(output.packages?["org.appfair.app.Net_Skip"], "missing app")
252+
let _ = netSkip
253+
//XCTAssertEqual("Skip Showcase", netSkip.metadata.name?["en-US"]) // oops
254+
255+
let skipNotes = try XCTUnwrap(output.packages?["org.appfair.app.SkipNotes"], "missing app")
256+
XCTAssertEqual("Skip Notes", skipNotes.metadata.name?["en-US"])
257+
251258
//let catalog: AltCatalog = try SourceCommand.CreateCommand.Output(fromJSON: output.utf8Data, dateDecodingStrategy: .iso8601)
252259
//dbg("catalog:", try? catalog.toJSON(outputFormatting: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes], dateEncodingStrategy: .iso8601).utf8String)
260+
261+
// TODO: validate against fdroidserver's `fdroid update` output for the app:
262+
let expected = try JSONDecoder().decode(FDroidIndex.self, from: """
263+
{
264+
"repo": {
265+
"name": {
266+
"en-US": "The App Fair Project"
267+
},
268+
"description": {
269+
"en-US": "This is a repository of apps to be used with F-Droid. Applications in this repository are either official binaries built by the original application developers, or are binaries built from source by the admin of f-droid.org using the tools on https://gitlab.com/fdroid."
270+
},
271+
"icon": {
272+
"en-US": {
273+
"name": "/icons/icon.png",
274+
"sha256": "7b42abdb1ec052f24f6957b73789355bdf5d1ba9d9c432d636a515838aa16989",
275+
"size": 681
276+
}
277+
},
278+
"address": "https://api.appfair.net/fdroid/repo",
279+
"timestamp": 1760571293000,
280+
"categories": {
281+
"server": {
282+
"name": {
283+
"en-US": "server"
284+
}
285+
}
286+
}
287+
},
288+
"packages": {
289+
"org.appfair.app.Tune_Out": {
290+
"metadata": {
291+
"added": 1760571293000,
292+
"categories": [
293+
"server"
294+
],
295+
"lastUpdated": 1760571293000,
296+
"name": {
297+
"en-US": "TuneOut"
298+
},
299+
"preferredSigner": "082e7b25ea1120bfb1b5e72a15dd359a5300b7b7d44297271ee0c870285bff38"
300+
},
301+
"versions": {
302+
"dcf83b18561745baecb8013f542d883dab7d1d1b3cf391173603add9c78df809": {
303+
"added": 1760571293000,
304+
"file": {
305+
"name": "https://github.com/appfair/Tune-Out/releases/download/1.0.5/TuneOut-release.apk",
306+
"sha256": "dcf83b18561745baecb8013f542d883dab7d1d1b3cf391173603add9c78df809",
307+
"size": 22206307
308+
},
309+
"manifest": {
310+
"nativecode": [
311+
"arm64-v8a",
312+
"armeabi",
313+
"armeabi-v7a",
314+
"mips",
315+
"mips64",
316+
"x86",
317+
"x86_64"
318+
],
319+
"versionName": "1.0.5",
320+
"versionCode": 17,
321+
"usesSdk": {
322+
"minSdkVersion": 28,
323+
"targetSdkVersion": 36
324+
},
325+
"signer": {
326+
"sha256": [
327+
"082e7b25ea1120bfb1b5e72a15dd359a5300b7b7d44297271ee0c870285bff38"
328+
]
329+
},
330+
"usesPermission": [
331+
{
332+
"name": "android.permission.INTERNET"
333+
},
334+
{
335+
"name": "android.permission.FOREGROUND_SERVICE"
336+
},
337+
{
338+
"name": "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"
339+
},
340+
{
341+
"name": "android.permission.POST_NOTIFICATIONS"
342+
},
343+
{
344+
"name": "android.permission.WAKE_LOCK"
345+
},
346+
{
347+
"name": "android.permission.ACCESS_NETWORK_STATE"
348+
},
349+
{
350+
"name": "org.appfair.app.Tune_Out.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
351+
}
352+
]
353+
}
354+
}
355+
}
356+
}
357+
}
358+
}
359+
""".data(using: .utf8)!)
360+
361+
XCTAssertEqual(1, expected.packages?.count)
253362
}
254363

255364
func testValidateCommand() async throws {

scripts/package_fairtool.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ cat > ${TOOLNAME}-bin/${TOOLNAME} << "EOF"
3333
# This scipt invokes the tool named after the script
3434
# in the appropriate OS and architecture sub-folder
3535
set -e
36-
TOOLNAME="$(basename "${BASH_SOURCE[0]}")"
37-
TOOLPATH="$(dirname "${BASH_SOURCE[0]}")"
36+
SCRIPTPATH="$(realpath "${BASH_SOURCE[0]}")"
37+
TOOLNAME="$(basename "${SCRIPTPATH}")"
38+
TOOLPATH="$(dirname "${SCRIPTPATH}")"
3839
OS="$(uname -s)"
3940
ARCH="$(uname -m)"
4041
PROGRAM="${TOOLPATH}"/"${OS}"/"${ARCH}"/"${TOOLNAME}"

0 commit comments

Comments
 (0)