SwiftGen / templates

Stencil templates for SwiftGen
MIT License
13 stars 38 forks source link

Make an Info.plist template for UIAppFonts #70

Open AliSoftware opened 7 years ago

AliSoftware commented 7 years ago

It would be nice to have a template able to auto-generate the content of the UIAppFonts key.

People could then generate the partial plist using swiftgen, then use e.g. PlistBuddy to merge that with their actual Info.plist:

# Generate the partial plist containing the entries for UIAppFonts
swiftgen fonts -t InfoPlist Resources/Fonts -o UIAppFonts.plist
# Delete any potential UIAppFont entry from Info.plist, then create a fresh one and merge it with the generate list
/usr/libexec/PlistBuddy -c "Delete UIAppFonts" -c "Add UIAppFonts array" -c "Merge UIAppFonts.plist UIAppFonts" "$INFOPLIST_PATH"

(Note: The second step here is automated using PlistBuddy, but in practice the developer might prefer just doing that step manually, by opening the UIAppFonts.plist file in Xcode, and copy/pasting that entry in their Info.plist manually)

The template to generate the UIAppFonts plist could look like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>{% for fam in families %}{% for font in fam.fonts %}
    <string>{% if param.useRelativePath %}{{font.path}}{% else %}{{font.path|basename}}{% endif %}</string>
{% endfor %}{% endfor %}</array>
</plist>
AliSoftware commented 7 years ago

See also SwiftGen/SwiftGen#324

djbe commented 7 years ago

Those "Delete", "Add" and "Merge" commands work that way? TIL

AliSoftware commented 7 years ago

Yup just tested that with a dummy plist.

I don't think there's another way to just replace the content of an array in a PLIST other than trying to delete, then add empty entry then merge. Just merge would just add to existing, with duplicating every time. I haven't seen an option for replacing with content of a file in one command

AliSoftware commented 7 years ago

Another option would be to make the template not generate the partial UIAppFonts.plist file, but a shell file with the commands directly:

#!/bin/sh
/usr/libexec/PlistBuddy -c "Delete UIAppFonts" -c "Add UIAppFonts array"{% for font in fonts %} -c "Add UIAppFonts: string '{{font.path}}'"{% endfor %} "$INFOPLIST_PATH"

And then people could just $(swiftgen fonts -t AddToPlist Resources/Fonts). But I don't like making people execute arbitrary generated code. That definitely smells like a security vulnerability. So I don't think that second solution would be a good idea.

djbe commented 7 years ago

Hey, when we have the pipe support (context loading and output), we can combine commands: swiftgen fonts generate plist | swiftgen plist read info.plist --output info.plist

AliSoftware commented 7 years ago

You mean generate the Fonts JSON context once, then use that context to generate the Fonts Swift constants in one hand, the Info.plist keys on another hand?

Or you mean use the pipe just for Info.plist synching, by generating the UIAppFonts keys in stdout and piping that to insert in Info.plist without an intermediate file?

If that's the latter, I don't think we'd use SwiftGen to inject the UIAppFonts entry in the Info.plist. We should still use PlistBuddy or another similar tool. At least not the future swiftgen plist subcommand, which would be intended to read a plist file, because if we did that that would mean having a template that is guaranteed to regenerated the exact same output as the input when going from Plist -> Parsed Context -> Plist, just injecting the UIAppFonts keys in the intermediate context. By reading the plist and re-generating another plist from what we read we risk loosing information or falling into edge cases. While using PlistBuddy -c Merge is guaranteed to just add the key, without touching the rest, instead of read-then-recreate.

djbe commented 7 years ago

Ehm, I mean:

djbe commented 7 years ago

But if plistbuddy already accepts from stdin and a file at the same time --> better

AliSoftware commented 7 years ago

I was actually looking at that, PlistBuddy doesn't seem to accept reading from stdin out of the box. We should investigate more.

But as I said, your 2nd point — which seems to be what I originally understood you meant — isn't a reliable solution. You're referring to the open PRs adding the plist subcommand, but those subcommands are meant to parse a PLIST file and generate a context from them. Typically in order to generate Swift constants from those PLIST keys. If you use that hypothetical future swiftgen plist subcommand with a template that would generate a PLIST as output, that means that you're going the PLIST -> Context -> PLIST route, and thus re-writing existing keys in the process, potentially in a lossy conversion process along the way.

For example, XML comments, CDATA encoding, etc, could potentially be lost in the process of parsing the input then trying to regenerate the same output from the parsed input. That's the same as if you tried to convert a PNG to a JPEG then back to a PNG, you'd lose some info in the process, especially because of the intermediate step during the conversion (which in our case would be the Stencil context).

So I don't think using the future swiftgen plist command for anything than generating Swift code from PLIST, especially for generating the same PLIST as the input PLIST, + additional content, would be a good idea. And that would even complexity the template because that would mean that we should recreate all the keys (and subkeys, recursively) parsed… except UIAppFont which mustn't be duplicated from the original plist but from the keys from stdin… quite a convoluted way to just merge 2 plists. (And in the end we would only be able to generate XML plists anyway, not binary ones, so lots of drawbacks!)

djbe commented 7 years ago

Right, didn't think of binary plists, or other data. But keep in mind that info.plist is parsed and edited by Xcode, so that too probably throws away any comments and other stuff.

The plist (and json) parsers don't do anything more than loading those entire dictionaries/arrays into a context, and the template could just go through that structure, and when it finds the key UIAppFont, instead of pasting the "current" info plist value, it can output the structure from the new (generated) UIAppFont plist.

But you're right, I might be trying to do stuff the hard way instead of using existing tools. Just thinking of the possibilities 😄

AliSoftware commented 7 years ago

Found the trick to use with pipe and avoid the intermediate file:

The template will look like this, generating the commands for PlistBuddy

Delete UIAppFonts
Add UIAppFonts array
{% for fam in families %}{% for font in fam.fonts %}Add UIAppFonts: string '{{font.path}}'{% endfor %}{% endfor %}
Save

And the user can call SwiftGen that way — using the template to generate PlistBuddy commands to stdout, and piping that to PlistBuddy itself to execute them:

swiftgen font -t InfoPlist Resources/Fonts | /usr/libexec/PlistBuddy "$INFOPLIST_PATH" >/dev/null

And that's it! 🎉

PlistBuddy will interpret what it receives from stdin (i.e. what gets generated by the SwiftGen template) as commands to apply on the $INFOPLIST_PATH file. The redirect >/dev/null is just to silence the Command: prompt otherwise printed by PlistBuddy for each command.

AliSoftware commented 7 years ago

To answer @bsarrazin 's questions/concerns, one could also imagine using those templates and still some commands in the Script Build Phase on every build… but just to warn in case the UIAppFonts keys differ, without auto-modifying the Info.plist and still letting the developer modify it manually.

For example one could use such a Script Build Phase:

# Generate the UIAppFonts.plist file, an simple plist with only the UIAppFonts key auto-generated
swiftgen -t UIAppFonts Resources/Fonts -o Generated/UIAppFonts.plist
# Compare the current content of the Info.plist's UIAppFonts key with the generated UIAppFonts.plist
# and emit an Xcode warning if they differ
if (/usr/libexec/PlistBuddy -c "Print UIAppFonts" "$INFOPLIST_FILE" | diff -q Generated/UIAppFonts.plist -); then
  echo "$INFOPLIST_FILE 's UIAppFonts key seems up-to-date"
else
  echo "warning: Some fonts are missing in the UIAppFonts key of $INFOPLIST_FILE. Copy/Paste the content of Generated/UIAppFonts.plist in that Info.plist file to update the list."
fi

With that kind of script, if the Info.plist's UIAppFonts key content differ from the UIAppFonts.plist file generated on each build, then that will make Xcode emit a warning and suggest the developer to update their Info.plist manually. They could do so by simply copy/pasting the content of the UIAppFonts.plist file into their Info.plist but at least they do it manually themselves (to avoid any risk that could happen when automating modification of a non-generated file like Info.plist)

So in the end once again developers can do whatever they want with that generated UIAppFonts.plist, it's up to them to write the Script Build Phase — or invoke some commands manually in the terminal when they feel the need — around SwiftGen to do whatever they want, but that won't be related to SwiftGen itself ;)