Esta página explica la delegación de ámbito. Se supone que has leído y comprendido las páginas anteriores, Definiciones de construcción y Ámbitos.
Ahora que hemos cubierto todos los detalles de los ámbitos, podemos explicar la
resolución de .value
en detalle. Te puedes saltar esta sección si es la
primera vez que lees esta página.
Resumamos lo que hemos aprendido hasta ahora:
Zero
utilizado por los tres ejes
de ámbito.
ThisBuild
utilizado únicamente por
el eje de subproyecto.
- Test
extiende Runtime
y Runtime
extiende la configuración Compile
.
- Una clave definida en build.sbt tiene como ámbito a
${current subproject} / Zero / Zero
de forma predeterminada.
- Una clave puede especificar un ámbito utilizando el operador /
.
Ahora supongamos que tenemos la siguiente definición de construcción:
lazy val foo = settingKey[Int]("")
lazy val bar = settingKey[Int]("")
lazy val projX = (project in file("x"))
.settings(
foo := {
(Test / bar).value + 1
},
Compile / bar := 1
)
Dentro del cuerpo de la entrada de foo
, la clave con ámbito Test / bar
ha sido declarada.
Sin embargo, a pesar de que Test / bar
no está definida en projX
, sbt sigue
siendo capaz de resolver Test / bar
utilizando otra clave con ámbito,
lo que lleva a que foo
sea inicializado a 2
.
sbt tiene un camino alternativo bien definido llamado delegación de ámbito. Esto permite establecer un valor una única vez en un ámbito más general, permitiendo que ámbitos más específicos hereden tal valor.
Estas son las reglas para la delegación de ámbito:
Zero
, que es la versión tarea-nula con ámbito de este ámbito.
Zero
(equivalente a un eje
de configuración sin ámbito).
ThisBuild
y luego Zero
.
Estudiaremos cada regla en el resto de esta página.
En otras palabras, dados dos ámbitos candidatos, si uno de ellos tiene un valor más específico en el eje de subproyecto entonces dicho eje siempre ganará sin importar el ámbito de la configuración o la tarea. De forma similar, si los subproyectos son los mismos, ganará el que tenga un valor más específico en el eje de configuración, sin importar lo que tenga en el ámbito de tarea. Veremos más reglas para definir más específico.
sustituyendo el eje de tarea en el siguiente orden:
el ámbito de dicha tarea y luego
Zero
, que es la versión tarea-nula con ámbito de este ámbito.
Aquí tenemos una regla concreta que muestra cómo sbt empleará la delegación de ámbitos
dada una clave.
Recuerda, estamos intentando mostrar el camino de búsqueda a partir de un
(xxx / yyy).value
cualquiera.
Ejercicio A: Dada la siguiente definición de construcción:
lazy val projA = (project in file("a"))
.settings(
name := {
"foo-" + (packageBin / scalaVersion).value
},
scalaVersion := "2.11.11"
)
¿Cuál es el valor de projA / name
?
"foo-2.11.11"
"foo-2.12.18"
La respuesta es "foo-2.11.11"
.
Dentro de .settings(...)
, scalaVersion
tiene automáticamente un ámbito de
projA / Zero / Zero
, por lo que packageBin / scalaVersion
se convierte en
projA / Zero / packageBin / scalaVersion
.
Esa clave con ámbito en particular no está definida. Utilizando la regla 2, sbt
sustituirá el eje de tarea a Zero
como projA / Zero / Zero
o
(projA / scalaVersion
).
Esta clave con ámbito está definida como "2.11.11"
.
Zero
(equivalente a un eje
de configuración sin ámbito).
El ejemplo es el projX
que vimos antes:
lazy val foo = settingKey[Int]("")
lazy val bar = settingKey[Int]("")
lazy val projX = (project in file("x"))
.settings(
foo := {
(Test / bar).value + 1
},
Compile / bar := 1
)
El ámbito completo es projX / Test / Zero
.
Además, recordemos que Test
extiende Runtime
y que Runtime
extiende
Compile
.
Test / bar
no está definido pero, debido a la Regla 3, sbt buscará bar
con
ámbito projX / Test / Zero
, projX / Runtime / Zero
y finalmente
projX / Compile / Zero
. Este último es encontrado, el cual es Compile / bar
.
ThisBuild
y luego Zero
.
Ejercicio B: Dada la siguiente definición de construcción:
ThisBuild / organization := "com.example"
lazy val projB = (project in file("b"))
.settings(
name := "abc-" + organization.value,
organization := "org.tempuri"
)
¿Cuál es el valor de projB / name
?
"abc-com.example"
"abc-org.tempuri"
La respuesta es abc-org.tempuri
.
Aplicando la Regla 4, el primer intento se hace mirando organization
con
ámbito projB / Zero / Zero
, el cuál está definido en projB
como
"org.tempuri"
.
Éste tiene mayor precedencia que la configuración a nivel de construcción
ThisBuild / organization
.
Ejercicio C: Dada la siguiente definición de construcción:
ThisBuild / packageBin / scalaVersion := "2.12.2"
lazy val projC = (project in file("c"))
.settings(
name := {
"foo-" + (packageBin / scalaVersion).value
},
scalaVersion := "2.11.11"
)
¿Cuál es el valor de projC / name
?
"foo-2.12.2"
"foo-2.11.11"
La respuesta es foo-2.11.11
.
scalaVersion
con ámbito projC / Zero / packageBin
no está definida.
La Regla 2 encuentra projC / Zero / Zero
.
La regla 4 encuentra ThisBuild / Zero / packageBin
.
En este caso la Regla 1 dice que el valor más específico del eje de subproyecto
gana, el cual es projC / Zero / Zero
, que está definido como "2.11.11"
.
Ejercicio D: Dada la siguiente definición de construcción:
ThisBuild / scalacOptions += "-Ywarn-unused-import"
lazy val projD = (project in file("d"))
.settings(
test := {
println((Compile / console / scalacOptions).value)
},
console / scalacOptions -= "-Ywarn-unused-import",
Compile / scalacOptions := scalacOptions.value // added by sbt
)
¿Qué saldría si ejecutamos projD / test
?
List()
List(-Ywarn-unused-import)
La respuesta es List(-Ywarn-unused-import)
.
La Regla 2 encuentra projD / Compile / Zero
,
la Regla 3 encuentra projD / Zero / console
,
y la Regla 4 encuentra ThisBuild / Zero / Zero
.
La Regla 1 elige projD / Compile / Zero
porque tiene tiene projD
en el eje de subproyecto y dicho eje tiene mayor
precedencia que el eje de tarea.
Después, Compile / scalacOptions
hace referencia a scalacOptions.value
,
luego necesitamos encontrar un delegado para projD / Zero / Zero
.
La Regla 4 encuentra ThisBuild / Zero / Zero
que finalmente resuelve a
(-Ywarn-unused-import)
.
Para saber qué está pasando puedes utilizar el comando inspect
.
sbt:projd> inspect projD / Compile / console / scalacOptions
[info] Task: scala.collection.Seq[java.lang.String]
[info] Description:
[info] Options for the Scala compiler.
[info] Provided by:
[info] ProjectRef(uri("file:/tmp/projd/"), "projD") / Compile / scalacOptions
[info] Defined at:
[info] /tmp/projd/build.sbt:9
[info] Reverse dependencies:
[info] projD / test
[info] projD / Compile / console
[info] Delegates:
[info] projD / Compile / console / scalacOptions
[info] projD / Compile / scalacOptions
[info] projD / console / scalacOptions
[info] projD / scalacOptions
[info] ThisBuild / Compile / console / scalacOptions
[info] ThisBuild / Compile / scalacOptions
[info] ThisBuild / console / scalacOptions
[info] ThisBuild / scalacOptions
[info] Zero / Compile / console / scalacOptions
[info] Zero / Compile / scalacOptions
[info] Zero / console / scalacOptions
[info] Global / scalacOptions
Fíjate en que “Provided by” muestra que projD / Compile / console / scalacOptions
es proporcionado por projD / Compile / scalacOptions
.
Además, bajo “Delegates”, todos los posibles candidatos son listados en orden
de precedencia.
projD
en el eje de subproyecto son listados
primero, luego ThisBuild
y luego Zero
.
Compile
en el eje de
configuración son listados primero y luego los de Zero
.
console
y luego los que
no lo tienen.
Fíjate en que la delegación de ámbito se parece mucho a la herencia de clases de
un lenguaje orientado a objetos, aunque con una diferencia:
En un lenguaje orientado a objetos como Scala, cuando existe un método llamado
drawShape
en un trait Shape
sus subclases pueden sobrescribir el
comportamiento incluso cuando drawShape
es utilizado por otros métodos en el
trait, lo es lo que se llama enlace dinámico.
Sin embargo, en sbt la delegación de ámbito puede delegar un ámbito a uno más general, como una configuración a nivel de proyecto hacia una configuración a nivel de construcción, pero dicha configuración a nivel de construcción no puede hacer referencia a la configuración a nivel de proyecto.
Ejercicio E: Dada la siguiente definición de construcción:
lazy val root = (project in file("."))
.settings(
inThisBuild(List(
organization := "com.example",
scalaVersion := "2.12.2",
version := scalaVersion.value + "_0.1.0"
)),
name := "Hello"
)
lazy val projE = (project in file("e"))
.settings(
scalaVersion := "2.11.11"
)
¿Qué devolverá projE / version
?
"2.12.2_0.1.0"
"2.11.11_0.1.0"
La respuesta es 2.12.2_0.1.0
.
projD / version
delega en ThisBuild / version
,
que a su vez depende de ThisBuild / scalaVersion
.
Debido a esto, la configuración a nivel de construcción debería limitarse
únicamente a asignaciones simples de valores.
Ejercicio F: Dada la siguiente definición de construcción:
ThisBuild / scalacOptions += "-D0"
scalacOptions += "-D1"
lazy val projF = (project in file("f"))
.settings(
compile / scalacOptions += "-D2",
Compile / scalacOptions += "-D3",
Compile / compile / scalacOptions += "-D4",
test := {
println("bippy" + (Compile / compile / scalacOptions).value.mkString)
}
)
¿Qué mostrará projF / test
?
"bippy-D4"
"bippy-D2-D4"
"bippy-D0-D3-D4"
La respuesta es "bippy-D0-D3-D4"
.
Esta es una variación de un ejercicio creado originalmente por
Paul Phillips.
Es una gran demostración de todas las reglas porque someKey += "x"
se expande
a
someKey := {
val old = someKey.value
old :+ "x"
}
Al obtener el valor antiguo se dispara la delegación y debido a la Regla 5
se irá a otra clave con ámbito.
Librémonos del +=
primero y anotemos los delegados para valores antiguos:
ThisBuild / scalacOptions := {
// Global / scalacOptions <- Regla 4
val old = (ThisBuild / scalacOptions).value
old :+ "-D0"
}
scalacOptions := {
// ThisBuild / scalacOptions <- Regla 4
val old = scalacOptions.value
old :+ "-D1"
}
lazy val projF = (project in file("f"))
.settings(
compile / scalacOptions := {
// ThisBuild / scalacOptions <- Reglas 2 y 4
val old = (compile / scalacOptions).value
old :+ "-D2"
},
Compile / scalacOptions := {
// ThisBuild / scalacOptions <- Reglas 3 y 4
val old = (Compile / scalacOptions).value
old :+ "-D3"
},
Compile / compile / scalacOptions := {
// projF / Compile / scalacOptions <- Reglas 1 y 2
val old = (Compile / compile / scalacOptions).value
old :+ "-D4"
},
test := {
println("bippy" + (Compile / compile / scalacOptions).value.mkString)
}
)
Esto se convierte en:
ThisBuild / scalacOptions := {
Nil :+ "-D0"
}
scalacOptions := {
List("-D0") :+ "-D1"
}
lazy val projF = (project in file("f"))
.settings(
compile / scalacOptions := List("-D0") :+ "-D2",
Compile / scalacOptions := List("-D0") :+ "-D3",
Compile / compile / scalacOptions := List("-D0", "-D3") :+ "-D4",
test := {
println("bippy" + (Compile / compile / scalacOptions).value.mkString)
}
)