-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
OnSystem: Add UsesOnSystem class #19721
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
base: main
Are you sure you want to change the base?
Conversation
59e913c
to
e9b1180
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great, a much improved API, nice work @samford!
e9b1180
to
f0b7523
Compare
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
`OnSystem` is exercised by other tests that include its modules but this adds some baseline tests to ensure some of these methods work as expected when tested in isolation. Between these added tests and existing tests, we should have 100% coverage when run on Homebrew/brew CI.
This adds a `UsesOnSystem` class to `OnSystem`, containing boolean instance variables to indicate which types of on_system methods are used in a formula or cask. This is intended as a replacement for `@on_system_blocks_exist`, which doesn't allow us to determine what kinds of on_system calls were used. This provides more granularity but we can still use `@uses_on_system.present?` to determine whether any on_system calls were used (and this doubles as a `nil` check in `Formula`, as the `self.class` instance variable has to use a nilable type). The `UsesOnSystem` instance variables cover the current `ARCH_OPTIONS` and `BASE_OS_OPTIONS`. At the moment, we mostly need to tell whether there are macOS/Linux or Intel/ARM on_system calls, so I've omitted instance variables for specific macOS version until we have a need for them. As a practical example, if you wanted to determine whether a cask uses Linux on_system calls, you can call `cask.uses_on_system.linux?`. The `linux` boolean will be `true` if the cask has an `on_linux` block, an `on_system` block (which requires Linux), or uses `os linux: ...`. This is something that would be challenging to determine from outside of `OnSystem` but it's relatively easy to collect the information in `OnSystem` methods and make it available like this.
f0b7523
to
e6038c9
Compare
When determining macOS requirements for a cask, we may need to reference requirements from related on_system blocks (e.g., `on_monterey :or_older`, `on_ventura`, `on_sonoma :or_newer`) when `depends_on macos` isn't adequate. Sometimes casks specify different `depends_on macos` values in macOS on_system blocks but that value is only set when the cask is loaded in an environment that satisfies the on_system block's requirements. There are other casks that contain macOS on_system blocks and use `depends_on macos` outside of the on_system blocks but it may only use the macOS version of the lowest on_system block (e.g., `>= :monterey`), which isn't sufficient if the cask's values vary based on macOS version. To be able to simulate macOS versions that meet the requirements of all the on_system blocks in a cask, we need to collect the macOS requirements in a way that doesn't require OS simulation. This is also something that's easy to do in on_system methods, so this adds a `macos_requirements` array to `UsesOnSystem`, containing `MacOSRequirement` objects created from the cask's macOS on_system block conditions.
76cca72
to
2edaeac
Compare
Finally coming back to this. The latest push:
This is another one of those times where writing tests took notably longer than the implementation 😆. There may be more room for improvement here but it should be ready for review at this point. |
This adds a `#highest_allowed` method to `MacOSRequirement`, so we can easily identify the highest supported macOS version that a requirement allows, if any. This can be used when producing a minimal set of supported macOS versions that meet requirements in `UsesOnSystem.macos_requirements`. One of the intended use cases for this is to identify which macOS versions we could simulate to work with all variations of a cask that uses macOS on_system blocks.
This adds more type signatures to `MacOSRequirement` to move it closer to `typed: strict`. There are a few areas that aren't quite clear to me based on the code and existing tests, so I've done what I can at the moment.
The `OnSystem.os_condition_met?` method only handles Linux and macOS values for the current OS, so this fails when testing with a generic OS. This shortcoming is only being surfaced now because there weren't any tests for `OnSystem` before. This addresses the issue by accounting for macOS values (`:macos` or a symbol from `MacOSVersion::SYMBOLS`) and returning `false` for any other values (`:linux`, `:generic`, etc.).
2edaeac
to
473f448
Compare
One thing worth discussing is whether |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, nice work @samford!
I can see what you mean about the naming; uses_on_system
feels maybe too close to uses_on_macos
given the two are unrelated? I like on_system_conditional
as you've suggested.
Don't feel strongly though! Happy to merge as-is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great!
Agreed about the naming being a little funky. Another possibility might be to call this class OnSystemDeclarations
, and then you can do formula.on_system_declarations.any?
or .none?
or .linux?
, etc
I like this 👍🏻 |
Any objections to omitting I'm happy to add |
I see your point on the
Sorry, I'm not sure I understand exactly what you mean
The latter. The reason I suggested On the other hand, when I see I don't feel strongly about this, though, as everyone's intuition may be different! This is just my own |
This may just be me but I tend to think of My interpretation of What I mean by "they would need to accept an argument or block" is that That's just my perspective and my understanding may be lacking or incorrect, so I'm open to other views. |
It should be (very) easy to implement Enumerable, as from the document you've linked:
The module would handle all of the rest which should require less work than present/blank/any/none/etc. manually. |
One way to approach it looks like this (note that Sorbet has additional requirements for implementing class UsesOnSystem < T::Struct
include Enumerable
extend T::Generic
Elem = type_member(:out) do
{ fixed: T.any(T::Boolean, T::Set[MacOSRequirement]) }
end
# [Omitted property declarations and aliases...]
# Returns a `Hash` of all non-`nil` instance variables, using `String` keys.
sig { returns(T::Hash[String, Elem]) }
def to_hash
T.let(serialize, T::Hash[String, Elem])
end
# Returns a `Hash` of all non-`nil` instance variables, using `Symbol` keys.
sig { returns(T::Hash[Symbol, Elem]) }
def to_h = to_hash.transform_keys(&:to_sym)
# Calls a provided block with each property value. If a block is not
# provided, it returns a new `Enumerator` instead.
sig {
override.params(
block: T.nilable(T.proc.params(arg0: Elem).returns(BasicObject)),
).returns(T.anything)
}
def each(&block)
block ? to_hash.values.each(&block) : to_hash.values.each
end
# Whether the object has only default values.
sig { returns(T::Boolean) }
def empty? = none?
# Whether the object has any non-default values.
sig { returns(T::Boolean) }
def present? = any?
end I initially implemented This provides all of the With that in mind, it seems like we have a couple of choices if we want to implement
Having to always conditionally initialize sig {
override.params(
block: T.nilable(T.proc.params(arg0: Elem).returns(BasicObject)),
).returns(T::Boolean)
}
def all?(&block)
block ? super : super(&:present?)
end
sig {
override.params(
block: T.nilable(T.proc.params(arg0: Elem).returns(BasicObject)),
).returns(T::Boolean)
}
def any?(&block)
block ? super : super(&:present?)
end
sig {
override.params(
block: T.nilable(T.proc.params(arg0: Elem).returns(BasicObject)),
).returns(T::Boolean)
}
def none?(&block) = !any?(&block)
sig {
override.params(
block: T.nilable(T.proc.params(arg0: Elem).returns(BasicObject)),
).returns(T::Boolean)
}
def one?(&block)
block ? super : super(&:present?)
end Thoughts on this? |
@samford Thanks for the example! I don't feel strongly either way given that; I defer to either using Enumerable or not depending on what works best for you. |
brew style
with your changes locally?brew typecheck
with your changes locally?brew tests
with your changes locally?This adds a
UsesOnSystem
class toOnSystem
, containing boolean instance variables to indicate which types of on_system methods are used in a formula or cask. This is intended as a replacement for@on_system_blocks_exist
, which doesn't allow us to determine what kinds of on_system calls were used. This provides more granularity but we can still use@uses_on_system.present?
to determine whether any on_system calls were used (and this doubles as anil
check inFormula
, as theself.class
instance variable has to use a nilable type).The
UsesOnSystem
instance variables cover the currentARCH_OPTIONS
andBASE_OS_OPTIONS
.UsesOnSystem
also provides amacos_requirements
set, which is used to keep track of macOS requirements that are specified inon_system
andon_<macos_version>
method calls (e.g.,on_system :linux, macos: :ventura_or_newer
,on_monterey :or_older
,on_ventura
,on_sonoma :or_newer
). This can be used to identify macOS dependency variations in a way that doesn't require simulating different OSs when loading the package.As a practical example, if you wanted to determine whether a cask uses Linux on_system calls, you can call
cask.uses_on_system.linux?
. Thelinux
boolean will betrue
if the cask has anon_linux
block, anon_system
block (which requires Linux), or usesos linux: ...
. This is something that would be challenging to determine from outside ofOnSystem
but it's relatively easy to collect the information inOnSystem
methods and make it available like this.Edit: I resolved the
Method uses_on_system does not exist on Cask::Cask
error by runningbrew typecheck --update
and committing the RBI change (it suddenly occurred to me while I was doing something completely unrelated 😆).