Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added: command plugin to download model files. #79

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions Package@swift-5.9.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// swift-tools-version: 5.9
import PackageDescription
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Soto (as with SwiftNIO) we look to support the last three versions of Swift. So for the moment that'd be 5.8, 5.9 and 5.10. Currently with your setup the download plugin is only available for 5.9, not 5.10. I would instead create a Package.@swift-5.8.swift which is a copy of the current Package.swift and make this version the default Package.swift

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, with the current package setup, if the current Swift version doesn't match any version-specific manifest, the package manager will pick the manifest with the most compatible tools version. This means the package manager will pick Package.swift for Swift 5.8 and Package@swift-5.9.swift for Swift 5.9 and above, as its tools version will be most compatible with future versions of the package manager. I read this in the documentation here.

In terms of readability and maintainability, I agree with your approach of keeping the 5.8 version tag instead of 5.9.


let package = Package(
name: "soto-codegenerator",
platforms: [.macOS(.v10_15)],
products: [
.executable(name: "SotoCodeGenerator", targets: ["SotoCodeGenerator"]),
.plugin(name: "SotoCodeGeneratorPlugin", targets: ["SotoCodeGeneratorPlugin"]),
.plugin(name: "SotoCodeModelDownloaderPlugin",targets: ["SotoCodeModelDownloaderPlugin"])
],
dependencies: [
.package(url: "https://github.com/soto-project/soto-smithy.git", from: "0.3.1"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-mustache.git", from: "1.0.3"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"),
],
targets: [
.executableTarget(
name: "SotoCodeGenerator",
dependencies: [
.byName(name: "SotoCodeGeneratorLib"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Logging", package: "swift-log")
]
),
.target(
name: "SotoCodeGeneratorLib",
dependencies: [
.product(name: "SotoSmithy", package: "soto-smithy"),
.product(name: "SotoSmithyAWS", package: "soto-smithy"),
.product(name: "HummingbirdMustache", package: "hummingbird-mustache"),
.product(name: "Logging", package: "swift-log")
]
),
.plugin(
name: "SotoCodeGeneratorPlugin",
capability: .buildTool(),
dependencies: ["SotoCodeGenerator"]
),
.plugin(
name: "SotoCodeModelDownloaderPlugin",
capability: .command(
intent: .custom(
verb: "get-soto-models",
description: "Download the required Model file schema required for soto code genrator to work"),
permissions: [
.writeToPackageDirectory(reason: "Write the Model files into target project"),
.allowNetworkConnections(
scope: .all(ports: []),
reason: "The plugin needs to download resource's from remote server"
)
])
),
.testTarget(
name: "SotoCodeGeneratorTests",
dependencies: ["SotoCodeGeneratorLib"]
)
]
)
142 changes: 142 additions & 0 deletions Plugins/SotoCodeModelDownloaderPlugin/Downloader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Soto for AWS open source project
//
// Copyright (c) 2017-2023 the Soto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Soto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
/// Downloads file from GitHub repositories.
struct GitHubResource {

/// The input folder containing the GitHub repository URL.
var inputFolder: String

/// The output folder where the downloaded files will be saved.
var outputFolder: String

/// An array of expected services to be downloaded.
var expectedServices: [String] = []
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved

/// The base URL for the GitHub API.
static var gitApi: URL { URL(string: "https://api.github.com/repos")! }
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved

/// trigger Downloading files from the GitHub repository.
func download() async throws {

// Ensure that the input folder contains a GitHub repository URL.
guard inputFolder.contains("github.com") else { return }
guard let gitFolderUrl = URL(string: inputFolder) else { return }
guard let (user, repo, directory, ref) = try extractGitComponents(from: gitFolderUrl.absoluteString) else { return }
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved

// Fetch the list of files from the GitHub repository.
let files = try await gitTreesApi(user: user, repository: repo, directory: directory)

// Create the output folder if it does not exist.
if !FileManager.default.fileExists(atPath: outputFolder) {
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved
try FileManager.default.createDirectory(atPath: outputFolder, withIntermediateDirectories: true)
}

// Download files concurrently using a task group.
try await withThrowingTaskGroup(of: Void.self) { group in
for index in 0..<files.count {
group.addTask {
if let filepath = files[index] as? String {
let escapingPath = filepath.replacingOccurrences(of: "#", with: "%23")
let path = [user, repo, ref, escapingPath].joined(separator: "/")
if let name = escapingPath.components(separatedBy: "/").last {
print("Downloading: " + name)
}
try await fetchFileWith(path: path, directory: escapingPath)
}
}
}
try await group.waitForAll()
}
}

/// Fetches the list of files from the GitHub repository using the GitHub Trees API.
private func gitTreesApi(user: String, repository: String, directory: String) async throws -> [Any] {

let directory = directory.components(separatedBy: "/").last ?? ""
var requestUrlString = GitHubResource.gitApi.absoluteString
requestUrlString.append("/\(user)/\(repository)/git/trees/HEAD?recursive=1")
guard let requestUrl = URL(string: requestUrlString) else { return [] }

let (data, _) = try await URLSession.shared.data(from: requestUrl)

let contents = try JSONSerialization.jsonObject(with: data) as? [String: Any]
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved

guard let tree = contents?["tree"] as? [Any] else { return [] }
var filePaths = [String]()
for item in tree {
guard let item = item as? [String: Any] else { continue }
let type = (item["type"] as? String) ?? ""
let path = (item["path"] as? String) ?? ""
let serviceName = (path.components(separatedBy: "/")
.last?.components(separatedBy: ".").first) ?? ""
if !expectedServices.isEmpty , !expectedServices.contains(serviceName) {
continue
}
// Check for subdirectory
let currentDirectory = path.components(separatedBy: "/").dropLast().last ?? ""
if (type == "blob" && currentDirectory.elementsEqual(directory)){
filePaths.append(path);
}
}
return filePaths
}

/// Extracts user, repository, directory, and reference from the GitHub repository URL.
private func extractGitComponents(from url: String) throws -> (user: String, repo: String, directory: String, ref: String)? {
let pattern = #"github\.com\/([^\/]+)\/([^\/]+)\/(?:tree|blob)\/([^\/]+)\/(.*)"#
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved

let regex = try NSRegularExpression(pattern: pattern, options: [])

guard let match = regex.firstMatch(in: url, options: [], range: NSRange(location: 0, length: url.utf16.count)) else {
return nil
}

let nsUrl = url as NSString
let userRange = match.range(at: 1)
let repoRange = match.range(at: 2)
let refRange = match.range(at: 3)
let directoryRange = match.range(at: 4)

let user = nsUrl.substring(with: userRange)
let repo = nsUrl.substring(with: repoRange)
let ref = nsUrl.substring(with: refRange)
let directory = nsUrl.substring(with: directoryRange)

return (user, repo, directory, ref)
}

/// Downloads raw file from the GitHub repository.
private func fetchFileWith(path: String, directory: String) async throws {
guard let downloadURL = URL(string: "https://raw.githubusercontent.com/" + path) else { return }

let (data, _) = try await URLSession.shared.data(from: downloadURL)
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved

let fileName = directory.components(separatedBy: "/").last ?? directory
let filePath = outputFolder + "/\(fileName)"

FileManager.default.createFile(atPath: filePath, contents: data)
}
}

// MARK: - Model and Endpoint URLs -

enum Repo {
/// Model files github directory.
static let modelDirectory = "https://github.com/soto-project/soto/tree/c36f311add37d4868b6b1688d88d320a5626d6ef/models"
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved

/// Endpoints github directory.
static let endpointsDirectory = "https://github.com/soto-project/soto/tree/c36f311add37d4868b6b1688d88d320a5626d6ef/models/endpoints"
}
67 changes: 67 additions & 0 deletions Plugins/SotoCodeModelDownloaderPlugin/plugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Soto for AWS open source project
//
// Copyright (c) 2017-2024 the Soto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Soto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import PackagePlugin
import Foundation

@main
struct SotoCodeModelDownloader: CommandPlugin {

func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws {

// get config File
let configfile = context.package.targets.compactMap({ $0 as? SourceModuleTarget })
.compactMap({ $0.sourceFiles
.first(where: { $0.path.lastComponent
.contains("soto.config.json") })?.path }).first

guard let configfile else {
Diagnostics.error("can not find the soto.config.json file in the target")
return
}

// extracting services from config
let services = try getSerivesFrom(path: configfile.string)

let mainDirectory = configfile.removingLastComponent()
let outputFolder = mainDirectory.appending("aws").string

// Download Model files
let modelDownloader = GitHubResource(inputFolder: Repo.modelDirectory,
outputFolder: outputFolder + "/models",
expectedServices: services)
// Download Endpoint File
let endpointDownloader = GitHubResource(inputFolder: Repo.endpointsDirectory,
outputFolder: outputFolder)

try await withThrowingTaskGroup(of: Void.self) { group in
for resource in [endpointDownloader, modelDownloader] {
group.addTask {
try await resource.download()
}
}
try await group.waitForAll()
}
print("Downloaded resources in : \(outputFolder)")
}

private func getSerivesFrom(path: String) throws -> [String] {
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved
let data = try Data(contentsOf: URL(filePath: path))
guard let json = try JSONSerialization.jsonObject(with: data) as? [String : Any] else { return [] }
Arpit160399 marked this conversation as resolved.
Show resolved Hide resolved
if let services = json["services"] as? [String : Any] {
let servicesName = services.keys.map({ $0 })
return servicesName
}
return []
}
}