[Tooling] Add linter for AppLocalizedString not used in extension targets - #18427
Conversation
b742b3f to
9c50d64
Compare
| C737554027C80F1300C6E9A1 /* String+CondenseWhitespace.swift in Sources */, | ||
| 7335AC6D21220F0F0012EF2D /* FormattableUserContent.swift in Sources */, | ||
| 7335AC6221220E690012EF2D /* FormattableContentFactory.swift in Sources */, | ||
| 09DBEA55281336E10019724E /* AppLocalizedString.swift in Sources */, |
There was a problem hiding this comment.
This is me adding that AppLocalizedString.swift file (the one containing the implementation for the AppLocalizedString function) to the WordPressNotificationServiceExtension target, so I can call the function in WordPress/WordPressNotificationServiceExtension/Sources/NotificationService.swift below
| outputPaths = ( | ||
| ); | ||
| runOnlyForDeploymentPostprocessing = 0; | ||
| shellPath = "/bin/zsh -euo pipefail"; |
There was a problem hiding this comment.
Using zsh as the shell here (instead of sh) intentionally, because:
- This calls a Ruby script
- Which thus depends on the user's configuration of
rbenvorrvmor similar - And those tools are usually set up in
~/.zshrc
So using /bin/sh here would not read the user's ~/.zshrc, thus not have rbenv properly set up, and thus might not use the expected version of Ruby or fail.
There was a problem hiding this comment.
🤔 this assumes every developer uses ZSH and configured a Ruby version manager. I think it's reasonable to expect folks to use rbenv or rvm since they would likely get issues with the repo if they didn't, but I don't know how Fish users (if any) might deal with this change 🤔
I suppose one option is to "just ship it" and see if someone complains about it?
Or... could we use /usr/bin/env ruby directly? 🤔
There was a problem hiding this comment.
🤔 ❗ ❗
The script failed for me because it used system Ruby (how?) and (something I didn't realize when reading the code) xcodeproj is not available there.
In fact, assuming xcodeproj to be available in any user Ruby install doesn't seem safe. Folks would have it available locally to this project via Bundler, but they might not have ever had the need to run gem install xcodeproj.
There was a problem hiding this comment.
but they might not have ever had the need to run gem install xcodeproj.
I assumed that was true because we use Bundler and furthermore vendor our gems. Then I doubted myself... But, I just verified it by:
- Uninstalling
fastlaneandxcodeprojfrom my Ruby 2.7.6 - Running
bundle install - Checking if
xcodeprojis available – It no longer is - Running the script manually – It fails
There was a problem hiding this comment.
Or... could we use
/usr/bin/env rubydirectly? 🤔
No, that doesn't make sense because then the content of the shellScript would have to be actual Ruby code, but we need it DRY and in a script. (I tried that here...)
I think a next step might be to wrap the Ruby script call into a .sh script that uses bundle exec ruby ... and does all the necessary checks beforehand.
There was a problem hiding this comment.
Ahhh dammit I was afraid such case and portability issue would happen 😭
Not the first time I encounter the question of "How to make a script called by Xcode use the user's shell setup, especially so it knows about Ruby and Bundler". This is tricky because:
- We should probably not assume which shell the users use (like I did here with
zsh, considering that becausezshwas now the default on macOS, this would be a safe bet… but that discards anyone who decided to use an alternate shell likefishindeed.- Though to be honest personally if I were to use
fishI'd probably organize my rc files so thatzshstill works and configurerubyandbundler, and then my fish rc wouldsourcethat~/.zshrc(and possibly add fish-specific stuff after that), that wayzshwould still work as well even if my everyday shell werefish… - …but eh, that's another assumption I can't really make to ensure this is really portable
- Though to be honest personally if I were to use
- We can't use
/usr/bin/env rubyeither here, because that risks not only using the system ruby, but also not usingbundler(and thus picking up thexcodeprojgem installed in the project's bundle, as part of a cocoapods dependency anyway)- To be fair my current solution didn't call
bundlereither, as you noticed. That worked on my setup because I have a plugin configured in~/.zshrcthat automatically prependsbundle execto many known gems and executions or ruby scripts by default for me, which is probably why I missed that when working on my PR - But that's obviously yet again not an assumption we can make
- To be fair my current solution didn't call
- I'll try your suggestion of making the Script Build Phase use
/bin/sh -euo pipefail, and it just calling a.shscript that would wrap the calls tobundle execand to the ruby script.- But tbh I'm not super confident that this will work perfectly either, because the
.shshell spawned by Xcode is a child process of Xcode, not of the login shell and Terminal, so it would not depend on (nor inherit the setup of) the user's default shell (the one they use when launching theirTerminal.app). Such a childshshell spawned by Xcode would thus not have knowledge of the setup and config related tobundlerandrbenvthe user usually have for their default shell in Terminal… - And obviously we can't either make the our
.shwrapper script assume anything about the user's setup; so the potential ideas of configuringrbenvfrom that wrapper script (e.g. callingeval $(rbenv init -)orexport PATH=~/.rbenv/shims:$PATHetc) won't work either, as users might be usingrvminstead ofrbenvin their system…
- But tbh I'm not super confident that this will work perfectly either, because the
I've found this post which explains the issue (which confirms what I've encountered with this kind of cases in the past), and also provides some ideas… but it also relies on the assumption that the user a particular Ruby Version manager (rvm in that post's case)… so still not a universal solution.
Maybe I'll have to make my wrapper script just test which Ruby Version Manager tool is installed (test which rbenv / command -v rbenv vs command -v rvm and act accordingly) and cover both cases… (and here goes my plan for the day 😅 )
There was a problem hiding this comment.
@mokagio It took me most of the day to figure out a working (?) solution that should cover cases of folks using any shell, and folks using either rbenv or rvm, but the last 2 commits should finally work.
At least @jhnstn helped testing the setup of using rvm, and @twstokes helped testing the setup of using rbenv (see Slack thread in p1651001863417419/1650990034.879279-slack-C011BKNU1V5) and after a lot of trial and error tweaking my script to make it work, this finally seem to cover all setups properly 🤞
You can test the changes in Jetpack from this Pull Request by:
|
You can test the changes in WordPress from this Pull Request by:
|
- Script Build Phase added to each of the targets that were of type App Extension - In each of those target, the Script Build Phase just calls `Scripts/BuildPhases/LintAppLocalizedStringsUsage.rb` - That script will scan each source file that is built as part of said extension target and check if `NSLocalizedString` appears used in any of them, and generate an error if so, suggesting to use `AppLocalizedString` instead.
9c50d64 to
3b484b6
Compare
|
Enabling auto-merge because this PR touches the |
`pod install` made the file references be reordered alphabetically, which is nice… but is more likely to cause conflicts when the `pbxproj` file is changed in `trunk` while this PR is still pending
| outputPaths = ( | ||
| ); | ||
| runOnlyForDeploymentPostprocessing = 0; | ||
| shellPath = "/bin/zsh -euo pipefail"; |
There was a problem hiding this comment.
🤔 this assumes every developer uses ZSH and configured a Ruby version manager. I think it's reasonable to expect folks to use rbenv or rvm since they would likely get issues with the repo if they didn't, but I don't know how Fish users (if any) might deal with this change 🤔
I suppose one option is to "just ship it" and see if someone complains about it?
Or... could we use /usr/bin/env ruby directly? 🤔
| outputPaths = ( | ||
| ); | ||
| runOnlyForDeploymentPostprocessing = 0; | ||
| shellPath = "/bin/zsh -euo pipefail"; |
There was a problem hiding this comment.
🤔 ❗ ❗
The script failed for me because it used system Ruby (how?) and (something I didn't realize when reading the code) xcodeproj is not available there.
In fact, assuming xcodeproj to be available in any user Ruby install doesn't seem safe. Folks would have it available locally to this project via Bundler, but they might not have ever had the need to run gem install xcodeproj.
| outputPaths = ( | ||
| ); | ||
| runOnlyForDeploymentPostprocessing = 0; | ||
| shellPath = "/bin/zsh -euo pipefail"; |
There was a problem hiding this comment.
but they might not have ever had the need to run gem install xcodeproj.
I assumed that was true because we use Bundler and furthermore vendor our gems. Then I doubted myself... But, I just verified it by:
- Uninstalling
fastlaneandxcodeprojfrom my Ruby 2.7.6 - Running
bundle install - Checking if
xcodeprojis available – It no longer is - Running the script manually – It fails
| outputPaths = ( | ||
| ); | ||
| runOnlyForDeploymentPostprocessing = 0; | ||
| shellPath = "/bin/zsh -euo pipefail"; |
There was a problem hiding this comment.
Or... could we use
/usr/bin/env rubydirectly? 🤔
No, that doesn't make sense because then the content of the shellScript would have to be actual Ruby code, but we need it DRY and in a script. (I tried that here...)
I think a next step might be to wrap the Ruby script call into a .sh script that uses bundle exec ruby ... and does all the necessary checks beforehand.
Co-authored-by: Gio Lodi <giovanni.lodi42@gmail.com>
To make sure rbenv / rvm are set up if used by the user, and that the script is run via `bundle exec ruby <path>` so that the ruby script would also be able to reference gems (like `xcodeproj`) from the local bundle.
| }; | ||
| 09DBEA4D281333060019724E /* [Lint] Check AppLocalizedString usage */ = { | ||
| isa = PBXShellScriptBuildPhase; | ||
| alwaysOutOfDate = 1; |
There was a problem hiding this comment.
Used this (aka un-checking the "Based on dependency analysis" checkbox on Xcode's UI for the Build Phase) since it's not really possible to specify proper "Input Files" for that build phase to properly guess when it should run or not — after all, the reason why we have to rely on the ruby script and using the xcodeproj gem is to know which of those input files to lint in the first place! — so we we don't have much other option to tell Xcode to run it in all cases…
| ); | ||
| runOnlyForDeploymentPostprocessing = 0; | ||
| shellPath = "/bin/bash -eu"; | ||
| shellScript = "\"$SRCROOT/../Scripts/BuildPhases/runRubyScript\" \"LintAppLocalizedStringsUsage.rb\" \"${TARGET_NAME}\"\n"; |
There was a problem hiding this comment.
This is the new way I used to wrap the call to the script into proper ceremony to setup rbenv/rvm/bundle exec for its execution.
I could have just made it a RunLintAppLocalizedStringsUsage.sh script specific for that one, which would have the call to bundle exec ruby LintAppLocalizedStringUsage.rb "$TARGET_NAME" hardcoded in it, but I figured it could be useful to have something more generic like runRubyScript which could wrap any arbitrary ruby script, so that if in the future we need to add other Build Phases based on ruby scripts, this could be reused (and also, if that works well, I might steal it for some other personal OSS projects that might also use ruby scripts for script build phases… 😇 )
There was a problem hiding this comment.
+1. I like that this is generic 👍
| # | ||
|
|
||
| # Add `rbenv` and `rvm` binaries to PATH, so that we support both | ||
| export PATH="$HOME/.rbenv/shims:$HOME/.rvm/bin:$PATH" |
There was a problem hiding this comment.
At first I tried to add some logic to detect which of rbenv or rvm were installed, and call the setup commands appropriately for each and all (see attempt in parent commit a84b805) but that didn't work. E.g. command -v rbenv apparently failed to detect that rbenv was installed, even system-wise, when ran by Xcode — while it worked in Terminal — so it didn't go into the if in Tanner's Mac… (even though on my own Mac it did work)
So in the end I settled for a simpler solution after all, which is to add the paths to the shims and binaries for both cases in $PATH here to cover both bases, and then calling bundle exec ruby <script.rb> — which would, in both cases (rvm like rbenv), use the shims and thus the locally installed ruby instead of the system one (while I think that just bundle exec <script.rb> would have used the shebang at the to of the script.rb, and in such case I'm not sure which one /usr/bin/env ruby would end up using…? 🤷 )
See @mokagio's suggestion in #18427 (comment)
| echo "Running the script using 'bundle exec' ..." | ||
| cd "$(dirname "${BASH_SOURCE[0]}")" | ||
| bundle exec ruby "$@" | ||
| cd - |
There was a problem hiding this comment.
Could this cd - be omitted, given the script runs in its own ephemeral shell, or is it useful to have it here?
There was a problem hiding this comment.
Oh it could have been omitted indeed. It's just my muscle memory always balancing cd xyz with cd - for good etiquette that got the best of me not thinking it was indeed unnecessary in that case 😅
| ); | ||
| runOnlyForDeploymentPostprocessing = 0; | ||
| shellPath = "/bin/bash -eu"; | ||
| shellScript = "\"$SRCROOT/../Scripts/BuildPhases/runRubyScript\" \"LintAppLocalizedStringsUsage.rb\" \"${TARGET_NAME}\"\n"; |
There was a problem hiding this comment.
+1. I like that this is generic 👍
mokagio
left a comment
There was a problem hiding this comment.
Run locally and it worked 🎉
I'm going to approve so auto-merge kicks in and we can avoid conflicts in the project file 🤞
But, I have a curve ball to throw at you... Yesterday I was chatting with @jkmassel and he refreshed in my mind the vision of making the day-to-day work for product developers Ruby-less.
This new big of Ruby tooling goes in the opposite direction 😳 I wish I realized it when I saw the PR. Obviously, we are far from the point where we can no longer depend on Ruby, but that day will come eventually.
Any ideas how we could rewrite this script in the future to not use Ruby? Maybe Swift has some utility to parse the project file that we can adopt in place of the xcodeproj gem?
@mokagio I think there has been a port of the Another alternative would be to have the swift script not depend on the Xcodeproj package nor any other dependency and instead parse the project file directly ( |
|
Nice. This is useful info to look at when we'll be seriously considering getting rid of any Ruby dependency on the devs side 👍 |
|
Couldn't resist the nerd snipe 😅 #18462 (tbh I kinda jumped on the occasion as a good excuse to do some swift code, as it was a long time and I missed it 😇; was good fun and refreshing! 🙂) |

This work is part of paaHJt-2Ib-p2, and a follow up of the annoucement in paNNhX-nP-p2.
What
Adds a linter which checks that every source file included in an app extension target (e.g. Widgets, NotificationExtension, ShareExtension…) uses
AppLocalizedStringinstead ofNSLocalizedString, to be sure it uses the translations from the app bundle (as we put all ourLocalizable.stringsfile in the app bundle to share between them app and extension targets, and reduce the overall.ipasize)This also fixes 3 of such violations that the linter found in the process.
Screenshot of the linter in action
Another example on a file used in 4 different app extension targets
Related Past PRs
For reference, the following past PRs merged a while ago migrated call sites of
NSLocalizedStringtoAppLocalizedStringfor files used in extension targets.This is why the linter that this PR adds today only found 3 violations (introduced after these PRs landed) and not hundreds.
To Test
WordPresstarget in Xcode, and verify that there is no error (CI will already do that for you)WordPress/Classes/ViewRelated/Stats/Extensions/Double+Stats.swiftfile and replace one of theAppLocalizedStringcalls it uses withNSLocalizedStringWordPressShareExtensionscheme, and verify that the build fails and the error is shown in on the right line.WordPressShareExtensionscheme, Xcode actually also builds theWordPressapp target (given how that scheme is configured see Edit Scheme > Build tab), which in turns builds other app extensions that the.appwill embed in its bundle. Hence potentially many more targets being built depending on the schemes, and thus more instances of the errorScripts/BuildPhases/LintAppLocalizedStringsUsage.rbin your Terminal and observe that it will scan all the App Extension targets found in the project — and report the same errors as the ones in Xcode — and exits with status code 1 (echo $?)