Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

check: pin flutter version #577

Closed
wants to merge 1 commit into from
Closed

Conversation

chrisirhc
Copy link
Contributor

@chrisirhc chrisirhc commented Mar 22, 2024

This approach uses direnv and fvm to setup the environment with the pinned version of the repo.
When setup correctly, direnv will turn on fvm's pinned flutter version whenever entering the zulip-flutter repository (directory).
The .envrc does the following:

  • Use fvm's flutter version if it's available.
  • Check that whether the flutter binary version matches what was pinned for the repo.
  • Let users know if there's a mismatch of version so that the user can take action.

Other notes:

  • Power users can:
    • use their checked out flutter sdk (either their own custom path, configured in .envrc, or the system default)
    • disable the check if they know what they're doing and want to use a custom flutter installation

TODOs:

  • fix git commit styling
  • check whether this approach works with non-VSCode IDEs
    • Verified that it works by setting up a custom flutter path

Closes #15

@gnprice
Copy link
Member

gnprice commented Mar 22, 2024

Thanks for picking this up!

Since you're working on this, here's a couple of thoughts about our needs here which I think aren't yet explicit on the issue thread #15:

  • I'd really like the pinned version to apply when building or running the app — so flutter build, flutter run, and when hitting the "run" button or equivalent in an IDE — and not only when running tests.

    Similarly, it's common to use flutter test directly (or a "run this test" button in an IDE) rather than tools/check, because that gives control over which specific tests to run. If that uses a different version of Flutter than tools/check does, that's likely to be super confusing when one way doesn't reproduce the same failures as the other way.

    Accomplishing that may require some sort of setup step, which is OK (though I'll be interested in keeping that setup as clean as possible). The key is just that when someone clones the zulip-flutter repo and follows our setup instructions in the most straightforward way, they consistently get our pinned Flutter version across all the above ways of building or running our code.

  • It's pretty common for us to develop changes to contribute to Flutter upstream, and that's been quite valuable for us. That's currently an extremely low-friction thing to do: the flutter tool is already using your local Git clone of the Flutter repo, so you just start editing that — and committing, and using git reset to move to various branches, etc. — and the changes show up just as seamlessly as if you were making them in your zulip-flutter tree itself.

    So I'd quite like to preserve that property, or close to it. Given the nature of pinning, we'll probably need to introduce a nonzero amount of friction — you're not really using a pinned version once you start making edits — but I'd like to keep that friction to the absolute minimum possible. For example, having to do something simple to disable pinning (so that you can make changes) would be fine.

@chrisirhc
Copy link
Contributor Author

@gnprice I've outlined a prototype that addresses your concerns, and updated the description of this PR.
Please take a look. This PR is still in discussion phase. I plan to clean it up as we iron out some of the details.

This does introduce 2 dependencies in the setup for most users. Power users can skip this setup and just not use the pinned version at their own risk of using a mismatched flutter version.

@chrisirhc chrisirhc force-pushed the chua/fvm branch 3 times, most recently from a4b5060 to 91cae50 Compare March 22, 2024 07:59
@chrisirhc
Copy link
Contributor Author

chrisirhc commented Mar 22, 2024

Thinking on this PR more, I think there's some value with testing the commit with the latest commit on flutter. Something like check-flutter-nightly job. This helps with continuously verifying that zulip is compatible with newer versions of flutter and would allow you to skip testing it and just upgrade to a new (tested) version.
However, I saw that this behaviour is not supported on GitHub actions: actions/runner#2347 .
So I'm going to leave that check-flutter-nightly idea out for now.

I don't like that I'm introducing two tools (direnv, fvm) at the same time, so it's not as clean as I'd like. But this seems like it would be most dummy-proof (hopefully) for most newcomers to the repository.

Also, it seems like direnv unfortunately doesn't work with IntelliJ/Android Studio. So.. :/ Maybe I'll need to rethink this approach.

Use direnv and flutter to enforce and validate a pinned version of flutter.

This approach uses `direnv` and `fvm` to setup the environment with the pinned
version of the repo.  When setup correctly, `direnv` will turn on `fvm`'s
pinned flutter version whenever entering the zulip-flutter repository
(directory).
The `.envrc` does the following:
- Use fvm's flutter version if it's available.
- Check that whether the flutter binary version matches what was pinned for the
repo.
- Let users know if there's a mismatch of version so that the user can take
action.

Other notes:
- Power users can:
  - use their checked out flutter sdk (either their own custom path, configured
    in .envrc, or the system default)
  - disable the check if they know what they're doing and want to use a custom
    flutter installation
@chrisirhc chrisirhc marked this pull request as ready for review March 22, 2024 08:21
@chrisirhc chrisirhc changed the title WIP: pin flutter version check: pin flutter version Mar 22, 2024
@gnprice
Copy link
Member

gnprice commented Mar 22, 2024

Very interesting, thanks!

I installed direnv and played around with it a bit, and it seems great — a very solid design. I'd be happy to start relying on it.

I also installed fvm, and… am less impressed. It scatters files across my homedir (~/.fvm_flutter/ for the program itself, ~/fvm/ for its data), which is not a good first sign. And the way it manages versions doesn't seem very coherent across upgrades:

  • You can pick a specific version in .fvmrc (or as an argument to fvm use), just as this draft does, and fvm will install that version.
  • This creates a Flutter tree in a location like ~/fvm/versions/18340ea16/, named after the specific version.
  • But then the recommended way to upgrade Flutter versions is to run flutter upgrade.
  • And when I do that, I now have a Flutter tree at a path ~/fvm/versions/18340ea16/, which fvm sees as the version named 18340ea16, but which is not in fact at commit 18340ea16 — it's at the newer, upgraded commit 233dd97f1e:
$ fvm spawn 18340ea16 --version
Spawning version "18340ea16"...
Flutter 3.21.0-13.0.pre.14 • channel master • https://github.com/flutter/flutter.git
Framework • revision 233dd97f1e (2 hours ago) • 2024-03-22 17:11:25 -0400
Engine • revision 52142e4287
Tools • Dart 3.4.0 (build 3.4.0-261.0.dev) • DevTools 2.34.0-dev.12

It's likely we could find ways to hack around that to some degree in order to get reasonable behavior. Effectively that's part of what the check_flutter_version in this draft's .envrc is doing. But at that point I think there's very little work left that fvm is actually successfully doing for us.

So I'd be interested in seeing a version of this that uses direnv, and uses fvm for partial inspiration, but then handles for ourselves the tasks we'd otherwise be trying to get fvm to do.

@gnprice
Copy link
Member

gnprice commented Mar 22, 2024

So I'd be interested in seeing a version of this that uses direnv, and uses fvm for partial inspiration, but then handles for ourselves the tasks we'd otherwise be trying to get fvm to do.

Specifically, this could look like:

  • There's a subdirectory with a name like .sdk/, inside the project root. It's a Git worktree of the Flutter repo — much like ~/fvm/versions/VERSION/ is.
    • Optionally this could save a bit of disk space, and download bandwidth, by using fancy Git features to have its .git/objects/ reuse the data in an existing Flutter clone, much as fvm has each ~/fvm/versions/VERSION/ reuse what's in ~/fvm/cache.git/. But this is actually a pretty minor optimization — the downloads in bin/cache/, of the Dart SDK and the Flutter engine and so on, are an order of magnitude bigger than the .git/ directory anyway, at around 1.4GB vs. 149MB. So might as well skip in a first version.
  • Then there's the version number we've pinned. Ideally this would come directly from pubspec.yaml or pubspec.lock, to keep the upgrade process simple; but it could also come from a separate file, like .fvmrc in this draft, if that simplifies implementation.
  • So we'd have a little script inside tools/ that updates .sdk/, or creates it if necessary, to match the pinned version. This is the job fvm use does on first creation, but it would also handle switching versions.

@gnprice
Copy link
Member

gnprice commented Mar 22, 2024

Also, it seems like direnv unfortunately doesn't work with IntelliJ/Android Studio. So.. :/ Maybe I'll need to rethink this approach.

It should be a matter of telling the IDE what path to find Flutter at, right? Like this draft does for VS Code in .vscode/settings.json:

  "dart.flutterSdkPaths": [".fvm/flutter_sdk"],

I expect there's a corresponding setting we can set for IntelliJ / Android Studio. (It won't use direnv, but VS Code doesn't appear to be doing so either.)

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here's comments on specific items in the code. Looking forward to this feature!

Comment on lines +15 to +18
- name: Install FVM
run: |
brew tap leoafarias/fvm
brew install fvm
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fairly slow — this step took 27s in the latest job recorded at https://github.com/zulip/zulip-flutter/pull/577/checks . That seems to be because it's building fvm from source:

==> Downloading https://github.com/leoafarias/fvm/archive/3.1.1.tar.gz
==> Downloading from https://codeload.github.com/leoafarias/fvm/tar.gz/refs/tags/3.1.1
==> Installing fvm from leoafarias/fvm
==> /home/linuxbrew/.linuxbrew/Cellar/fvm/3.1.1/libexec/bin/dart pub get
==> /home/linuxbrew/.linuxbrew/Cellar/fvm/3.1.1/libexec/bin/dart compile exe -Dversion=3.1.1 bin/main.dart -o fvm
🍺  /home/linuxbrew/.linuxbrew/Cellar/fvm/3.1.1: 1,022 files, 572.2MB, built in 20 seconds

If we instead do the curl|bash command suggested as an alternative in the docs:

$ curl -fsSL https://fvm.app/install.sh | bash

then on my machine that takes about 1.5s. The basic reason is that it's downloading a published release artifact rather than a source tree:

# Download FVM
URL="https://github.com/leoafarias/fvm/releases/download/$FVM_VERSION/fvm-$FVM_VERSION-$OS-$ARCH.tar.gz"
if ! curl -L "$URL" -o fvm.tar.gz; then

I don't love running curl|bash installs. For this one, I downloaded the script first, read it, then ran it. But ultimately it's not any different from what brew tap leofarias/fvm; brew install fvm is doing — both are taking a script published by the same person, and running it.

Comment on lines +8 to +10
local fvm_flutter_version_string=$(cat .fvmrc)
# Fetch string from format `"flutter": "18340ea16c"``
local fvm_flutter_version=$(echo "$fvm_flutter_version_string" | grep -o '"flutter": "[a-z0-9]*"' | cut -d '"' -f 4)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior of cut is finicky to reason through. Since there's already a regexp describing what the line should look like, better to take that just slightly further to identify which part of the line we want:

Suggested change
local fvm_flutter_version_string=$(cat .fvmrc)
# Fetch string from format `"flutter": "18340ea16c"``
local fvm_flutter_version=$(echo "$fvm_flutter_version_string" | grep -o '"flutter": "[a-z0-9]*"' | cut -d '"' -f 4)
local fvm_flutter_version=$(
<.fvmrc perl -lne 'print $1 if (/"flutter": "([a-z0-9]*)"/)'
)

Then two other points:

  • should be a-f, not a-z :-) (and let's write it as [0-9a-f], so the digits are in order of their value)
  • a fine point of shell semantics which shellcheck will tell you about — more about that in my next comment

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Oh, and since the file being parsed here is actually JSON, the cleanest solution would be to use jq instead. But because this is a script that will run for all developers in this repo, best to do without that extra dependency. We do regularly use jq in scripts that only a smaller set of maintainers are expected to need to run.)

PATH_add .fvm/flutter_sdk/bin

# Check flutter version matches what's in .fvmrc
check_flutter_version() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is a shell script, so let's include it in tools/check shellcheck. That means in the run_shellcheck function, adding this file to both the files_check line and the targets array.

(Then that will give a few warnings on this version, which you can fix.)

# Fetch string from format `"flutter": "18340ea16c"``
local fvm_flutter_version=$(echo "$fvm_flutter_version_string" | grep -o '"flutter": "[a-z0-9]*"' | cut -d '"' -f 4)

local flutter_version_string=$(flutter --version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly flutter --version is fairly slow — which makes this take around 1.5 seconds when I cd into this directory. That'd get pretty annoying.

To handle that, see the workaround in print_header in tools/check. For any code there which you'd like to reuse, please feel free to move functions to a file in tools/lib/ (and/or an existing file there like tools/lib/git.sh).

Comment on lines +97 to +98
1. Install [direnv][direnv] and [fvm][fvm], for most
users, you can use Homebrew:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not assume "most users" are using Homebrew. :-) It's widely used on macOS, but I think not so much on Linux. I'm primarily on Linux (where I don't have Homebrew installed); and I think among Zulip contributors in general, Linux is most common, followed by Windows, with relatively few on macOS.

Fine to include Homebrew instructions as an example. For direnv, let's mention sudo apt install direnv as another example.

Comment on lines +25 to +26
- name: Load direnv (.envrc)
uses: HatsuneMiku3939/direnv-action@v1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this action do? Can we just say apt install direnv, and then a line or two from the direnv setup instructions?

I'd be more comfortable with that, because it makes it a lot more transparent what's happening.

Alternatively, we could just add an item to PATH directly, without direnv — the real value of direnv is in its automation for interactive shell use as people enter and leave the directory.

@@ -0,0 +1,27 @@
# This file is used by direnv to setup the environment when entering to use fvm's flutter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do keep prose to around 68 columns wide, and at most 70. It makes a real difference in readability — for example here's what the commit message looks like in my terminal:
image

Even when the terminal is wide and the lines don't wrap, prose wider than around 70 columns gets less comfortable to read as your eyes have to scan back and forth across the lines.

This guideline is explicit in our commit style guide:
https://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html#formatting-guidelines
but it applies to prose elsewhere, like Markdown and comments, too.

@chrisirhc
Copy link
Contributor Author

Agree with the sentiment around fvm. I found myself working around unexpected behaviour — what felt like buggy behaviour.
I'm testing an alternative approach using git submodules. I initially avoided it because it seemed a bit archaic with fvm around, but based your comments, I think it's worth exploring that option.
Testing that out via #579 . If it looks good, I may replace this PR with that approach.

@gnprice
Copy link
Member

gnprice commented Mar 23, 2024

Cool, sounds good.

I'm a little wary of Git submodules, because my experience has been that Git's UX with them can be annoying. But fundamentally they do implement pretty much exactly the design I sketched in #577 (comment) , without requiring us to write much or any code for it. So that sounds like a good experiment to try.

And if we like the design but decide the Git submodules UX is annoying, we can always try replacing it later with our own implementation.

@chrisirhc
Copy link
Contributor Author

chrisirhc commented Mar 23, 2024

I'm a little wary of Git submodules, because my experience has been that Git's UX with them can be annoying.

Yes, same, I had somewhat bad memories from prior usage though it could've been minor annoyances in the grander scheme of things.

But fundamentally they do implement pretty much exactly the design I sketched in #577 (comment) , without requiring us to write much or any code for it. So that sounds like a good experiment to try.

Yes it does look pretty much like that. #579 is working. I'll take a stab at what README instructions could look like.

And if we like the design but decide the Git submodules UX is annoying, we can always try replacing it later with our own implementation.

+1

@chrisirhc
Copy link
Contributor Author

chrisirhc commented Mar 23, 2024

I expect there's a corresponding setting we can set for IntelliJ / Android Studio. (It won't use direnv, but VS Code doesn't appear to be doing so either.)

Yep, it does have a corresponding setting ✅

image

I didn't find way to easily set this up programmatically via the .idea files, but it could be mentioned via the README instructions.

@chrisirhc
Copy link
Contributor Author

chrisirhc commented Mar 23, 2024

#579 is shaping up to look promising.
I've added a pros/cons in the PR's description. If that looks good, I can move forward with that approach/PR instead.

@gnprice
Copy link
Member

gnprice commented Mar 25, 2024

Yep, it does have a corresponding setting ✅

I didn't find way to easily set this up programmatically via the .idea files, but it could be mentioned via the README instructions.

Cool. Yeah, that screen looks familiar — I think I've had to set something there whenever first setting up Flutter and Android Studio on a given machine. It corresponds to a step in Flutter's own setup instructions:
image

So we'll definitely need to write some clear instructions for it, but it shouldn't be too much of a burden.


I also spent some time fiddling to try to figure out what file in IntelliJ's config controls that setting. IntelliJ's config setup is kind of a mess, but what I expected from past experience was:

  • I change the setting.
  • I look for config files that have a recent last-modified timestamp, and/or files that contain something looking like the value I just set.
  • One of those is clearly the setting.
  • If we're lucky, the file it's in consists only of stuff that's reasonable to share in version control. Add that file to version control.

For more background, see commit 62a0d7c.

Instead, I couldn't find the setting anywhere! Even rg -uuu vendor/flutter .idea/ didn't find anything relevant — all the matches were more-specific paths that seem to be used for more-specific purposes.

The closest match I found was this, over in the config that lives in a global place in my homedir:

$ grep -B5 -A3 vendor/flutter ~/.config/Google/AndroidStudio2023.1/options/other.xml 
  "keyToStringList": {
    "DART_SDK_KNOWN_PATHS": [
      "/home/greg/n/flutter/flutter/bin/cache/dart-sdk"
    ],
    "FLUTTER_SDK_KNOWN_PATHS": [
      "/home/greg/z/flutterz/vendor/flutter",
      "/home/greg/n/flutter/flutter",
      "/home/greg"
    ],

But that's a list of several possible values; it looks like it might be the list of values it'll suggest in that very dropdown.

Anyway 🤷 I think we'll be OK with the manual setup step.

@chrisirhc
Copy link
Contributor Author

chrisirhc commented Mar 26, 2024

#579 supersedes this PR. See #579 's description for trade-offs considered in that approach compared to this PR's.
I've incorporated the code review comments in this PR into #579 .

@chrisirhc chrisirhc closed this Mar 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pin a precise version of Flutter
2 participants