Repository navigation
Expand file tree
/
Copy pathSFD.ScriptTools.LibrarySetup.fsx
More file actions
executable file
·304 lines (250 loc) · 11.1 KB
/
Copy pathSFD.ScriptTools.LibrarySetup.fsx
File metadata and controls
executable file
·304 lines (250 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
#!/usr/bin/env -S dotnet fsi --
open System
open System.IO
open System.Diagnostics
open System.Text.RegularExpressions
// Constants
[<Literal>]
let DllName = "SFD.GameScriptInterface.dll"
let defaultOutputPath = Path.Combine(".", "lib", DllName)
let homeDir = Environment.GetFolderPath Environment.SpecialFolder.UserProfile
/// Default locations to probe for the DLL, in order of preference.
let defaultSearchPaths: string list =
[
// Linux Flatpak Steam
Path.Combine(
homeDir,
".var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/Superfighters Deluxe",
DllName
)
// Linux native Steam
Path.Combine(homeDir, ".local/share/Steam/steamapps/common/Superfighters Deluxe", DllName)
// Windows Steam
@"C:\Program Files (x86)\Steam\steamapps\common\Superfighters Deluxe\" + DllName ]
// Argument parsing
let printUsage () =
printfn "Usage: SFD.ScriptTools.LibrarySetup.fsx [-f|--file <path>] [-o|--output <path>] [-F|--force] [--dry-run] [-h|--help]"
printfn ""
printfn " -f, --file Explicit path to %s (skips auto-search)" DllName
printfn " -o, --output Output path for the symlink/copy (default: %s)" defaultOutputPath
printfn " -F, --force On version mismatch, update <RequiredGameSdkVersion> to the installed DLL version"
printfn " --dry-run Run checks without touching the filesystem; exit 1 if action is needed"
printfn " -h, --help Show this help message and exit"
let scriptArgs = fsi.CommandLineArgs |> List.ofArray
type ParsedArgs =
{ File: string option
Output: string option
Help: bool
DryRun: bool
Force: bool }
let rec parseArgs (args: string list) (acc: ParsedArgs) =
match args with
| [] -> acc
| ("-f" | "--file") :: value :: rest -> parseArgs rest { acc with File = Some value }
| ("-o" | "--output") :: value :: rest -> parseArgs rest { acc with Output = Some value }
| "--dry-run" :: rest -> parseArgs rest { acc with DryRun = true }
| ("-F" | "--force") :: rest -> parseArgs rest { acc with Force = true }
| ("-h" | "--help") :: rest -> parseArgs rest { acc with Help = true }
| unknown :: rest ->
eprintfn "Warning: Unknown argument '%s' (ignored)." unknown
parseArgs rest acc
let parsed =
parseArgs
scriptArgs
{ File = None
Output = None
Help = false
DryRun = false
Force = false }
if parsed.Help then
printUsage ()
exit 0
let dryRun = parsed.DryRun
let force = parsed.Force
let explicitFile = parsed.File
let outputPath = defaultArg parsed.Output defaultOutputPath
let currentDir = Directory.GetCurrentDirectory()
// Resolve which DLL to use: prefer an existing copy nested in the current
// directory (linking is then unnecessary), otherwise locate a source on disk.
let existingNestedCopies =
Directory.GetFiles(currentDir, DllName, SearchOption.AllDirectories)
let resolveSourcePath () : string option =
match explicitFile with
| Some path ->
if File.Exists path then
Some(Path.GetFullPath path)
else
eprintfn "Specified file via -f does not exist: %s" path
None
| None -> defaultSearchPaths |> List.tryFind File.Exists |> Option.map Path.GetFullPath
let dllPath, skipLinking =
if existingNestedCopies.Length > 0 then
printfn "'%s' already exists within the current directory:" DllName
for p in existingNestedCopies do
printfn " %s" p
printfn "Skipping link/copy step."
existingNestedCopies.[0], true
else
match resolveSourcePath () with
| Some p -> p, false
| None ->
eprintfn "Could not find '%s' in any of the default locations:" DllName
for p in defaultSearchPaths do
eprintfn " %s" p
eprintfn ""
eprintfn "Please specify its location manually with -f <path>."
exit 1
// Create output folder(s) and link/copy the DLL
let fullOutputPath = Path.GetFullPath outputPath
let outputDir = Path.GetDirectoryName fullOutputPath
let didLink =
if skipLinking then
false
else
if not (String.IsNullOrEmpty outputDir) && not (Directory.Exists outputDir) then
if dryRun then
printfn "[dry-run] Would create directory: %s" outputDir
else
Directory.CreateDirectory outputDir |> ignore
printfn "Created directory: %s" outputDir
if File.Exists fullOutputPath then
printfn "Output file already exists at: %s" fullOutputPath
printfn "Nothing to link/copy."
false
else
printfn "Found source DLL at: %s" dllPath
printfn "Linking to: %s" fullOutputPath
if dryRun then
printfn "[dry-run] Would create symlink (falling back to a copy)."
false
else
try
File.CreateSymbolicLink(fullOutputPath, dllPath) |> ignore
printfn "Successfully created symlink."
true
with ex ->
printfn "Symlink creation failed (%s)." ex.Message
printfn "Falling back to copying the file instead..."
try
File.Copy(dllPath, fullOutputPath, overwrite = false)
printfn "Successfully copied the DLL."
true
with copyEx ->
eprintfn "Copy also failed: %s" copyEx.Message
exit 1
// Ensure the linked/copied DLL is gitignored (only when we actually linked it)
if didLink && not dryRun then
let gitignorePath = Path.Combine(currentDir, ".gitignore")
let gitignoreEntry =
Path.GetRelativePath(currentDir, fullOutputPath).Replace('\\', '/')
let existingGitignoreLines =
if File.Exists gitignorePath then
File.ReadAllLines gitignorePath
else
[||]
let alreadyIgnored =
existingGitignoreLines
|> Array.exists (fun line -> line.Trim() = gitignoreEntry)
if alreadyIgnored then
printfn "'.gitignore' already contains an entry for '%s'." gitignoreEntry
else if File.Exists gitignorePath then
let needsLeadingNewline =
existingGitignoreLines.Length > 0
&& not (String.IsNullOrEmpty(Array.last existingGitignoreLines))
let textToAppend =
(if needsLeadingNewline then Environment.NewLine else "")
+ gitignoreEntry
+ Environment.NewLine
File.AppendAllText(gitignorePath, textToAppend)
printfn "Added '%s' to existing .gitignore." gitignoreEntry
else
File.WriteAllText(gitignorePath, gitignoreEntry + Environment.NewLine)
printfn "Created .gitignore with entry '%s'." gitignoreEntry
// Version tracking: read the DLL's product version and keep it in sync with the
// <RequiredGameSdkVersion> field of the nearest .csproj.
/// ProductVersion from .NET's FileVersionInfo sometimes carries a trailing
/// NUL byte on Linux; strip it so the stored value is clean and comparable.
let sanitizeVersion (s: string) =
s.Replace("\u0000", "").Trim()
let assemblyVersion =
try
let fvi = FileVersionInfo.GetVersionInfo dllPath
let v = sanitizeVersion fvi.ProductVersion
if String.IsNullOrEmpty v then
eprintfn "ProductVersion of '%s' is empty; cannot determine version." dllPath
None
else
Some v
with ex ->
eprintfn "Failed to read version from '%s': %s" dllPath ex.Message
None
match assemblyVersion with
| None ->
eprintfn "Cannot determine the assembly version. Cannot continue."
exit 1
| Some v ->
printfn "DLL version: %s" v
let csprojFiles =
Directory.GetFiles(currentDir, "*.csproj", SearchOption.TopDirectoryOnly)
if csprojFiles.Length = 0 then
printfn "No .csproj found in '%s'; skipping version tracking." currentDir
printfn "Done."
exit 0
let csprojPath = csprojFiles.[0]
let csprojName = Path.GetFileName csprojPath
printfn "Project file: %s" csprojPath
let csprojText = File.ReadAllText csprojPath
let fieldRegex =
Regex(
@"<RequiredGameSdkVersion>\s*(.*?)\s*</RequiredGameSdkVersion>",
RegexOptions.Singleline
)
match fieldRegex.Match csprojText with
| m when m.Success ->
let fieldText = m.Groups.[1].Value.Trim()
if String.Equals(fieldText, v, StringComparison.Ordinal) then
printfn "RequiredGameSdkVersion (%s) matches the installed DLL." v
printfn "Done."
exit 0
else
eprintfn "Version mismatch!"
eprintfn " Project requires: %s" fieldText
eprintfn " Installed DLL: %s" v
if force && not dryRun then
let replacement = sprintf "<RequiredGameSdkVersion>%s</RequiredGameSdkVersion>" v
let newCsprojText = fieldRegex.Replace(csprojText, replacement, 1)
File.WriteAllText(csprojPath, newCsprojText)
printfn "Updated <RequiredGameSdkVersion> from '%s' to '%s' in '%s'." fieldText v csprojName
printfn "Done."
exit 0
elif force && dryRun then
eprintfn "[dry-run] Would update <RequiredGameSdkVersion> from '%s' to '%s'." fieldText v
eprintfn "Cannot continue without writing changes; exiting with status 1."
exit 1
else
eprintfn "Install the required SFD SDK version or update '%s' manually." csprojName
eprintfn "Pass --force to update the project to the installed version automatically."
exit 1
| _ ->
printfn "No <RequiredGameSdkVersion> field found in '%s'." csprojName
if dryRun then
eprintfn "[dry-run] Would add <RequiredGameSdkVersion>%s</RequiredGameSdkVersion>." v
eprintfn "Cannot continue without writing changes; exiting with status 1."
exit 1
// Insert the field right after the opening tag of the first <PropertyGroup>,
// inheriting its indentation (plus one level) so the file's formatting is
// preserved instead of having the whole document re-flowed by an XML parser.
let pgRegex = Regex @"([ \t]*)<PropertyGroup[^>]*>\r?\n"
match pgRegex.Match csprojText with
| pgm when pgm.Success ->
let indent = pgm.Groups.[1].Value + " "
let entry = sprintf "%s<RequiredGameSdkVersion>%s</RequiredGameSdkVersion>%s" indent v Environment.NewLine
let newCsprojText = csprojText.Insert(pgm.Index + pgm.Length, entry)
File.WriteAllText(csprojPath, newCsprojText)
printfn "Added <RequiredGameSdkVersion>%s</RequiredGameSdkVersion> to '%s'." v csprojName
printfn "Done."
exit 0
| _ ->
eprintfn "No <PropertyGroup> found in '%s'; cannot add the field." csprojName
eprintfn "Add <RequiredGameSdkVersion>%s</RequiredGameSdkVersion> manually." v
exit 1