Skip to content

Instantly share code, notes, and snippets.

@Sberm
Last active April 24, 2023 14:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Sberm/26f7c56144d8c76a8b972a8d1043ecc5 to your computer and use it in GitHub Desktop.
Save Sberm/26f7c56144d8c76a8b972a8d1043ecc5 to your computer and use it in GitHub Desktop.
[GSoC-2023 Proposal]Customizable Package Templates

Customizable Package Templates

Background

swift package init provides a fast way to initialize projects, users can select from several hard-coded templates to start with.

Motivation

There are many different needs for such templates, so there should be a way to customize these templates and share them with others without having to make changes to the SwiftPM codebase itself. It could also be useful to make templates interactive, bring user-friendliness to SwiftPM's CLI.

Proposed Solution

Define a fixed location to store local templates and using git repositories to store remote templates. Once local template is stored, use swift package init --type <projectName> to use. Use swift package init --url <YourTemplatesURL.git> to fetch remote templates. Once copied the template, swiftPM will decode template.json file(where interactive information is stored) and make init process interactive. We'll add interactivity to hard-coded projects as well.

Interactivity

Bringing interactivity to swiftPM's cli, such interactivity could be:

  1. Include tests
  2. Include plugins
  3. Include a set of dependencies
  4. Name of the project
  5. Type of template to start with

template.json

Use template.json to define the interactivity of the template,the format could be something like this:

  1. template name
"name" : "MyTemplate"
  1. dependencies
"dependencies" : [
  {
    "url": "https://github.com/wadetregaskis/FluidMenuBarExtra.git",
    "from":"1.0.1"
  },
  {
    "url": "https://github.com/SwiftBeta/SwiftOpenAI.git",
    "branch":"main"
  }
]
  1. include tests
"test" : [
  {
    "path" : "path/to/test/directory",
    "default_include" : "false",
    "interact" : "true"
  },
  {
    "path" : "path/to/another/test/directory",
    "default_include" : "true"
  }
]
  1. include plugins
"plugins" : [
  {
    "path" : "path/to/local/plugin",
    "interact" : "true"
  },
  {
    "url" : "https://github.com/apple/swift-docc-plugin",
    "default_include" : "false",
    "interact" : "true"
  }
]

Early progress

Selecting the type of template to include

Users can use arrow keys and enter to choose the type of template to start with. To enable that in terminal, we have to use Raw Mode Input to process keyboard input. Normally, one has to press enter in order to let program reads each line of input, such input mode is called canonical input. If wanting to read a key just after it is pressed, we have to change terminal's input mode to Raw Mode. Using c code to enable such is easier and more straightforward here.

  • Solution 1

Write ICANON flag in termios.c_lflag, use c_lflag &= ~(ICANON); to turn off canonical input.

#if os(Linux) || os(FreeBSD)
  raw.c_iflag &= ~UInt32(BRKINT | ICRNL | INPCK | ISTRIP | IXON)
  raw.c_oflag &= ~UInt32(OPOST)
  raw.c_cflag |= UInt32(CS8)
  raw.c_lflag &= ~UInt32(ECHO | ICANON | IEXTEN | ISIG)
#else
  raw.c_iflag &= ~UInt(BRKINT | ICRNL | INPCK | ISTRIP | IXON)
  raw.c_oflag &= ~UInt(OPOST)
  raw.c_cflag |= UInt(CS8)
  raw.c_lflag &= ~UInt(ECHO | ICANON | IEXTEN | ISIG)
#endif

Code referenced from here.

  • Solution 2

Using stty system command to change terminal characteristics. In this case, we'll use stty raw. To use the stty command, we need to use C. Integrate into swiftPM's source code using c interop.

system("stty raw");

This is the solution I used.

In new target folder RawModeInputLibc, I created rawModeInput.c, adding code below:

#include "include/rawModeInput.h"
#include <stdlib.h>

void setRawModeInput () {
    system("stty raw");
}

void setCookedModeInput() {
    system("stty cooked");
}

In Package.swift , add target and use it for dependency of workspace.

.target(
  name: "RawModeInputLibc"
),
.target(
  /** High level functionality */
  name: "Workspace",
  dependencies: [
    "Basics",
    "PackageFingerprint",
    "PackageGraph",
    "PackageModel",
    "PackageRegistry",
    "PackageSigning",
    "SourceControl",
    "SPMBuildCore",
    "RawModeInputLibc" // add dependency
  ],
  exclude: ["CMakeLists.txt"]
),

Add static public method interactiveInit() and private method processInput() in InitPackage.swift

public static func interactiveInit() throws -> PackageType{
        return try processInput()
}
    // for interactive init
    static let types = 7
    static let initMode: [PackageType] = [.library, .executable, .empty, .tool, .buildToolPlugin, .commandPlugin, .macro]
		// ANSI escape code
    static let plain = "\u{001B}[0m"
    static let yellow = "\u{001B}[38;5;202m"
    static var firstWrite = true
    static var initModeBool : [Bool] = []

    private static func processInput() throws -> PackageType {

        // set default mode library
        var currentMode = 0 

        for _ in 0...types {
            initModeBool.append(false)
        }

        initModeBool[currentMode] = true
        
        while true {
						// set canonical input, otherwise print will be weird.
            setCookedModeInput()

            if firstWrite == false {
                print("\u{001B}[\(types+1)A",terminator: "")
            }
            firstWrite = false

            print("Choose package type:")
            for packageType in 0...types - 1{
                ANSIPrint(type: packageType)
            }

            setRawModeInput()
            initModeBool[currentMode] = false
            let cha = getchar()
            // use q to quit
            if (cha == 113) {
                setCookedModeInput()
                exit(0)
            } else if (cha == 13) { // enter
                setCookedModeInput()
                return initMode[currentMode]
            } else if (cha == 27) { // arrow key up and down
                let cha2 = getchar()
                if (cha2 == 91) {
                    let cha3 = getchar()
                    if (cha3 == 65) { // up
                        currentMode = currentMode - 1 < 0 ? currentMode - 1 + types : currentMode - 1
                    } else if (cha3 == 66) { // down
                        currentMode = (currentMode + 1) % types
                    } 
                }
            }
            initModeBool[currentMode] = true
        }
    }

    private static func ANSIPrint (type: Int) throws {
        if (initModeBool[type] == true) {
            print(yellow, terminator: "")
        }
        print(initMode[type])
        print(plain,terminator: "")
    }

If I broke the programming code, I'm very sorry. I'm also very sorry if I use too many static.

This is for demonstration purposes only, if you feel like there are problems please let me know.

Compatibility

In order to use interactive init, cross-platform is a big problem, I will focus on this issue in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment