Skip to content

Commit 9ad9d67

Browse files
committed
New README.
1 parent 36fc70b commit 9ad9d67

10 files changed

+79
-85
lines changed

App/InjectionNext.xcodeproj/project.pbxproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@
806806
INFOPLIST_FILE = InjectionNext/Info.plist;
807807
LD_RUNPATH_SEARCH_PATHS = "@executable_path/../Frameworks";
808808
MACOSX_DEPLOYMENT_TARGET = 10.15;
809-
MARKETING_VERSION = 1.2.8;
809+
MARKETING_VERSION = 1.3.0;
810810
PRODUCT_BUNDLE_IDENTIFIER = com.johnholdsworth.InjectionNext;
811811
PRODUCT_NAME = "$(TARGET_NAME)";
812812
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -830,7 +830,7 @@
830830
INFOPLIST_FILE = InjectionNext/Info.plist;
831831
LD_RUNPATH_SEARCH_PATHS = "@executable_path/../Frameworks";
832832
MACOSX_DEPLOYMENT_TARGET = 10.15;
833-
MARKETING_VERSION = 1.2.8;
833+
MARKETING_VERSION = 1.3.0;
834834
PRODUCT_BUNDLE_IDENTIFIER = com.johnholdsworth.InjectionNext;
835835
PRODUCT_NAME = "$(TARGET_NAME)";
836836
PROVISIONING_PROFILE_SPECIFIER = "";

App/InjectionNext/FrontendServer.swift

+6-4
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,14 @@ class FrontendServer: InjectionServer {
180180

181181
DispatchQueue.main.async {
182182
if !projectRoot.hasSuffix(".xcodeproj") &&
183-
(AppDelegate.watchers.keys.first {
184-
projectRoot.hasPrefix($0) }) == nil {
183+
AppDelegate.alreadyWatching(projectRoot) == nil {
185184
let open = NSOpenPanel()
186-
open.prompt = "Watch Project Directory?"
185+
// open.titleVisibility = .visible
186+
// open.title = "InjectionNext: add directory"
187+
open.prompt = "InjectionNext - Watch Directory?"
187188
open.directoryURL = URL(fileURLWithPath: projectRoot)
188189
open.canChooseDirectories = true
189190
open.canChooseFiles = false
190-
// open.showsHiddenFiles = TRUE;
191191
if open.runModal() == .OK, let url = open.url {
192192
AppDelegate.ui.watch(path: url.path)
193193
}
@@ -199,11 +199,13 @@ class FrontendServer: InjectionServer {
199199
loggedFrontend = frontendPath
200200

201201
for source in primaries {
202+
// Don't update compilations while connected
202203
if InjectionServer.currentClient != nil &&
203204
recompiler.compilations.index(forKey: source) != nil {
204205
continue
205206
}
206207

208+
// Try to minimise memory churn
207209
if let previous = recompiler
208210
.compilations[source]?.arguments ?? lastArguments,
209211
args == previous {

App/InjectionNext/Info.plist

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<key>CFBundleShortVersionString</key>
2020
<string>$(MARKETING_VERSION)</string>
2121
<key>CFBundleVersion</key>
22-
<string>11164</string>
22+
<string>11206</string>
2323
<key>LSApplicationCategoryType</key>
2424
<string>public.app-category.developer-tools</string>
2525
<key>LSMinimumSystemVersion</key>

App/InjectionNext/InjectionHybrid.swift

+8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ extension AppDelegate {
4040
Self.lastWatched = path
4141
watchDirectoryItem.state = Self.watchers.isEmpty ? .off : .on
4242
}
43+
static func alreadyWatching(_ projectRoot: String) -> String? {
44+
return watchers.keys.first { projectRoot.hasPrefix($0) }
45+
}
46+
static func restartLastWatcher() {
47+
DispatchQueue.main.async {
48+
lastWatched.flatMap { watchers[$0]?.watcher?.restart() }
49+
}
50+
}
4351
}
4452

4553
class InjectionHybrid: InjectionBase {

App/InjectionNext/InjectionServer.swift

+4-5
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Popen
2020
class InjectionServer: SimpleSocket {
2121

2222
/// So commands from differnt threads don't get mixed up
23-
static let commandQueue = DispatchQueue(label: "InjectionCommand")
23+
static let clientQueue = DispatchQueue(label: "InjectionCommand")
2424
/// Current connection to client app. There can be only one.
2525
static weak var currentClient: InjectionServer?
2626

@@ -52,7 +52,7 @@ class InjectionServer: SimpleSocket {
5252

5353
// Send command to client app
5454
func sendCommand(_ command: InjectionCommand, with string: String?) {
55-
Self.commandQueue.async {
55+
Self.clientQueue.async {
5656
_ = self.writeCommand(command.rawValue, with: string)
5757
}
5858
}
@@ -135,7 +135,7 @@ class InjectionServer: SimpleSocket {
135135
} catch {
136136
self.error("\(self) error \(error)")
137137
}
138-
Self.commandQueue.sync {} // flush messages
138+
Self.clientQueue.sync {} // flush messages
139139
}
140140

141141
func processResponses() {
@@ -150,8 +150,7 @@ class InjectionServer: SimpleSocket {
150150
}
151151

152152
sendCommand(.xcodePath, with: Defaults.xcodePath)
153-
AppDelegate.lastWatched.flatMap {
154-
AppDelegate.watchers[$0]?.watcher?.restart() }
153+
AppDelegate.restartLastWatcher()
155154

156155
while true {
157156
let responseInt = readInt()

App/InjectionNext/NextCompiler.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class NextCompiler {
113113

114114
print("Prepared dylib: "+dylib)
115115
prepared[sourceName] = dylib
116-
InjectionServer.commandQueue.sync {
116+
InjectionServer.clientQueue.sync {
117117
guard let client = InjectionServer.currentClient else {
118118
AppDelegate.ui.setMenuIcon(.ready)
119119
return

App/interposable.png

160 KB
Loading

README.md

+55-70
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,46 @@ app without having to relaunch it. This can save a developer a significant
88
amount of time tweaking code or iterating over a design.
99

1010
This repo is a refresh of the [InjectionIII](https://github.com/johnno1962/InjectionIII)
11-
app that uses a different technique to determine how to rebuild source files
12-
that should be faster and more reliable for very large projects. Gone is the
13-
involved parsing of Xcode build logs (if you can locate them) and messy
14-
escaping of special characters in filenames. A new app is used to launch Xcode
15-
with a [SourceKit debugging flag](https://www.jpsim.com/uncovering-sourcekit/)
16-
enabled which provides all the information you need to be able to recompile
17-
files and then the runtime implementation of injection included in the
18-
[InjectionLite](https://github.com/johnno1962/InjectionLite) package looks after the rest.
11+
app that uses different techniques to determine how to rebuild source files
12+
that should be faster and more reliable for very large projects. With versions
13+
1.3.0+ the only changes that are required to your project are to add the
14+
following "Other Linker Flags" to your project's **Debug** build settings:
1915

16+
![Icon](App/interposable.png)
17+
18+
That last flag is to link what were bundles in InjectionIII as a dynamic library:
19+
20+
`/Applications/InjectionNext.app/Contents/Resources/lib$(PLATFORM_NAME)Injection.dylib`
21+
22+
If you want to inject on a device you'll also need to add the following
23+
as a "Run Script/Build Phase" of your main target to copy the required
24+
libraries into your app bundle (for a Debug build) and toggle "Enable Devices"
25+
to open a network port for incoming connections from your client app.
26+
27+
```
28+
export RESOURCES="/Applications/InjectionNext.app/Contents/Resources"
29+
if [ -f "$RESOURCES/copy_bundle.sh" ]; then
30+
"$RESOURCES/copy_bundle.sh"
31+
fi
32+
```
2033
The basic MO is to build the app in the `App` directory, or download one of
2134
the binary releases in this repo, move it /Applications, quit Xcode and run the
2235
resulting `InjectionNext.app` and use that to re-launch Xcode using the menu item
23-
`Launch Xcode` from the status bar. You then add this repo as a Swift package
24-
dependency of your project and that should be all that is required for injection
25-
in the simulator, injection on devices and injection of a MacOS app. No more
26-
code changes required to load binary code bundles etc and you can leave
27-
the InjectionNext package configured into your project permanently as
28-
its code is only included for a DEBUG build. Your code changes take effect
36+
`Launch Xcode` from the status bar. No more code changes required to load binary
37+
code bundles etc. Your code changes take effect
2938
when you save a source for an app that has this package as a dependency
30-
and has connected to the InjectIonNext app which has launched Xcode.
39+
and has connected to the InjectionNext app which has launched Xcode.
40+
41+
**Please note:** you can only inject changes to code inside a function body
42+
and you can not add/remove or rename properties with storage or add or
43+
reorder methods in a non final class or change function signatures.
3144

32-
As ever, it is important to add the options `-Xlinker` and `-interposable`
33-
(without double quotes and on separate lines) to the "Other Linker Flags" of
34-
the targets of your project (for the `Debug` configuration only) to enable
35-
"interposing". Otherwise, you will only be able to inject non-final class methods.
3645
To inject SwiftUI sucessfully a couple of minor code changes to each View are
3746
required. Consult the https://github.com/johnno1962/HotSwiftUI README or you
3847
can make these changes automatically using the menu item "Prepare SwiftUI/".
39-
40-
![Icon](App/interposable.png)
48+
For SwiftUI you would also generally also integrate either the
49+
[Inject](https://github.com/krzysztofzablocki/Inject) or
50+
[HotSwiftUI](https://github.com/johnno1962/HotSwiftUI) package into your project.
4151

4252
When your app runs it should connect to the `InjectionNext.app` and it's icon
4353
change to orange. After that, by parsing the messages from the "supervised"
@@ -50,67 +60,54 @@ phase of your build logs in the window that pops up. Sometimes a
5060
device will not connect to the app first time after unlocking it.
5161
If at first it doesn't succeed, try again.
5262

53-
If you'd rather not be adding a SPM dependency to your project, the app's
54-
resources contains pre-built bundles which you can copy into your app during
55-
the build by using a "Run Script/Build Phase" (while disabling the "user
56-
script sandboxing" build setting) such as the following:
63+
The colours of the menu bar icon bar correspond to:
5764

58-
```
59-
export RESOURCES="/Applications/InjectionNext.app/Contents/Resources"
60-
if [ -f "$RESOURCES/copy_bundle.sh" ]; then
61-
"$RESOURCES/copy_bundle.sh"
62-
fi
63-
```
64-
These bundles should load automatically if you've integrated the
65-
[Inject](https://github.com/krzysztofzablocki/Inject) or
66-
[HotSwiftUI](https://github.com/johnno1962/HotSwiftUI) packages into your project.
67-
Otherwise, you can add the following code to run on startup of your app:
65+
* Blue when you first run the InjectionNext app.
66+
* Purple when you have launched Xcode using the app.
67+
* Orange when your client app has connected to it.
68+
* Green while it is recompiling a saved source.
69+
* Yellow if the source has failed to compile.
6870

69-
```
70-
#if DEBUG
71-
if let path = Bundle.main.path(forResource:
72-
"iOSInjection", ofType: "bundle") ??
73-
Bundle.main.path(forResource:
74-
"macOSInjection", ofType: "bundle") {
75-
Bundle(path: path)!.load()
76-
}
77-
#endif
78-
```
79-
The binary bundles also integrate [Nimble](https://github.com/Quick/Nimble)
71+
The binary dylibs also integrate [Nimble](https://github.com/Quick/Nimble)
8072
and a slightly modified version of the [Quick](https://github.com/Quick/Quick)
8173
testing framework to inhibit spec caching under their respective Apache licences.
8274

83-
To inject tests on a device: use these bundles and, when enabling the
84-
"Enable Devices" menu item select "Enable testing on device" which
85-
will add the parameters shown to the link of each dynamic library.
86-
As you do this, the above command will be inserted into the clipboard
75+
To inject tests on a device: when enabling the
76+
"Enable Devices" menu item, select "Enable testing on device" which
77+
will add the arguments shown to the link of each dynamic library.
78+
As you do this, the command above will be inserted into the clipboard
8779
which you should add to your project as a "Run Script" "Build Phase"
88-
to copy the required libraries into the app bundle.
80+
of the main target to copy the required libraries into the app bundle.
8981

9082
### Cursor/VSCode mode.
9183

9284
If you would like to use InjectionNext with the Cursor code editor,
9385
you can have it fall back to InjectionIII-style log parsing using
9486
the "...or Watch Project" menu item to select the project root
95-
you will be working under. In this case you shouldn't launch
87+
you will be working under (or use the new "Proxy" mode below
88+
for Swift projects.) In this case, you shouldn't launch
9689
Xcode from inside the InjectionNext.app but you'll need to have
9790
built your app in Xcode at some point in the past for the logs
9891
to be available. You should build using the same version as that
9992
selected by `xcode-select`.
10093

101-
### Compiler "proxy" mode.
94+
### New compiler "proxy" mode.
10295

103-
It is also possible to intercept swift compilation commands as a proof of
104-
concept in case at some point in the future these are no longer captured in
96+
It is also possible to intercept swift compilation commands as a new proof of
97+
concept for when at some point in the future these are no longer captured in
10598
the Xcode logs (as was the case with Xcode 16.3 beta1). In this case, select
10699
"Intercept compiler" to patch the current toolchain slightly to capture all
107100
compilations using a script and send them to the InjectionNext.app. Once this
108101
patch has been applied you don't need to launch Xcode from the app and you can
109-
inject by starting a file watcher using the "...or Watch Project" menu item.
102+
inject by starting a file watcher using the "...or Watch Project" menu item
103+
(though this should happen automatically when you recompile Swift sources).
110104

105+
So, InjectionNext has three ways in which it can operate of which the newest and
106+
the simplest one if you're prepared to patch your toolchain is the "proxy" mode.
111107
The original mode of operation launching Xcode inside the app takes preference
112-
otherwise, if you have selected a file watcher and are intercepting the compiler
113-
commands that is next in line followed by the log parsing fallback which
108+
and, if you have selected a file watcher and are intercepting the compiler
109+
commands that is the next preference followed by the log parsing fallback using
110+
the [InjectionLite](https://github.com/johnno1962/InjectionLite) package which
114111
essentially works as InjectionIII did when the logs are available.
115112

116113
For more information consult the [original InjectionIII README](https://github.com/johnno1962/InjectionIII)
@@ -124,16 +121,4 @@ your project in Xcode automatically using the -projectPath option.
124121
Set a user default with the same name if you want to always open
125122
this project inside the selected Xcode on launching the app.
126123

127-
The colours of the menu bar icon bar correspond to:
128-
129-
* Blue when you first run the InjectionNext app.
130-
* Purple when you have launched Xcode using the app.
131-
* Orange when your client app has connected to it.
132-
* Green while it is recompiling a saved source.
133-
* Yellow if the source has failed to compile.
134-
135-
Please note: you can only inject changes to code inside a function body
136-
and you can not add/remove or rename properties with storage or add or
137-
reorder methods in a non final class or change function signatures.
138-
139124
The fabulous app icon is thanks to Katya of [pixel-mixer.com](http://pixel-mixer.com/).

0 commit comments

Comments
 (0)